From 7c7104d5049839a93037311a8bbec5b27a4c4e81 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:43:00 +0700 Subject: [PATCH 01/22] Add Default for bridge usage snapshots and shrink literals --- .../src-tauri/src/auto_resume_tests.rs | 8 +- .../src-tauri/src/commands/bridge.rs | 55 ++--------- .../src-tauri/src/commands/providers.rs | 36 +------ .../src/commands/providers/reset_backfill.rs | 36 +------ apps/desktop-tauri/src-tauri/src/powertoys.rs | 57 +----------- .../src-tauri/src/tray_bridge.rs | 93 ++----------------- .../src-tauri/src/tray_presentation_tests.rs | 71 ++------------ .../src-tauri/src/usage_metric.rs | 34 +------ 8 files changed, 32 insertions(+), 358 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/auto_resume_tests.rs b/apps/desktop-tauri/src-tauri/src/auto_resume_tests.rs index b8eadc0275f..6967729e71d 100644 --- a/apps/desktop-tauri/src-tauri/src/auto_resume_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/auto_resume_tests.rs @@ -35,15 +35,9 @@ mod tests { remaining_percent: 100.0 - used_percent, window_minutes: Some(300), resets_at: Some(Utc::now().to_rfc3339()), - reset_description: None, is_exhausted: exhausted, is_informational: informational, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() } } diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index b3839776e08..dea4cf29c59 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -53,6 +53,12 @@ fn default_full_remaining() -> f64 { 100.0 } +impl Default for RateWindowSnapshot { + fn default() -> Self { + Self::from_rate_window(&RateWindow::new(0.0)) + } +} + impl RateWindowSnapshot { pub(super) fn from_rate_window(rw: &RateWindow) -> Self { Self { @@ -273,7 +279,7 @@ pub struct ProviderDisplayDetailSnapshot { } /// A frontend-friendly snapshot of one provider's usage data. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProviderUsageSnapshot { #[serde(default)] @@ -588,47 +594,11 @@ impl ProviderUsageSnapshot { Self { provider_id: id.cli_name().to_string(), display_name: id.display_name().to_string(), - primary: RateWindowSnapshot { - used_percent: 0.0, - remaining_percent: 100.0, - window_minutes: None, - resets_at: None, - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, - }, primary_label: Some(metadata.session_label.to_string()), - secondary: None, - secondary_label: None, - model_specific: None, - tertiary: None, - tertiary_label: None, - extra_rate_windows: Vec::new(), - inventory: Vec::new(), - display_details: Vec::new(), - cost: None, - plan_name: None, - account_email: None, - subscription: None, - source_label: String::new(), - has_successful_claude_cli_quota: false, updated_at: chrono::Utc::now().to_rfc3339(), error: Some(error), error_state: state_kind, - pace: None, - account_organization: None, - tray_status_label: None, - fetch_duration_ms: None, - wayfinder_usage: None, - open_ai_api_usage: None, - session_equivalent_forecast: None, - quota_burndown: None, + ..Default::default() } } } @@ -1151,14 +1121,7 @@ mod tests { window_minutes, resets_at: resets_at.map(|dt| dt.to_rfc3339()), reset_description, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() } } diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers.rs b/apps/desktop-tauri/src-tauri/src/commands/providers.rs index 14457ae57d1..8d6999d63d5 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers.rs @@ -1611,15 +1611,7 @@ mod reset_backfill_tests { remaining_percent: 100.0 - used, window_minutes: Some(300), resets_at: resets_at.map(String::from), - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - description_is_detail: false, - monthly_limit_block: None, + ..Default::default() } } fn codex_snapshot(primary: RateWindowSnapshot) -> ProviderUsageSnapshot { @@ -1627,32 +1619,8 @@ mod reset_backfill_tests { provider_id: "codex".into(), display_name: "Codex".into(), primary, - primary_label: None, - secondary: None, - secondary_label: None, - model_specific: None, - tertiary: None, - tertiary_label: None, - extra_rate_windows: Vec::new(), - inventory: Vec::new(), - display_details: Vec::new(), - cost: None, - plan_name: None, - account_email: None, - subscription: None, - source_label: String::new(), - has_successful_claude_cli_quota: false, updated_at: "2026-01-01T00:00:00Z".into(), - error: None, - error_state: codexbar::core::ProviderStateKind::Ready, - pace: None, - account_organization: None, - tray_status_label: None, - fetch_duration_ms: None, - wayfinder_usage: None, - open_ai_api_usage: None, - session_equivalent_forecast: None, - quota_burndown: None, + ..Default::default() } } #[test] diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers/reset_backfill.rs b/apps/desktop-tauri/src-tauri/src/commands/providers/reset_backfill.rs index 1311db6344c..c0aa66729a5 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers/reset_backfill.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers/reset_backfill.rs @@ -103,15 +103,7 @@ mod tests { remaining_percent: 100.0 - used, window_minutes: Some(300), resets_at: resets_at.map(String::from), - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() } } @@ -120,32 +112,8 @@ mod tests { provider_id: "codex".into(), display_name: "Codex".into(), primary, - primary_label: None, - secondary: None, - secondary_label: None, - model_specific: None, - tertiary: None, - tertiary_label: None, - extra_rate_windows: Vec::new(), - inventory: Vec::new(), - display_details: Vec::new(), - cost: None, - plan_name: None, - account_email: None, - subscription: None, - source_label: String::new(), - has_successful_claude_cli_quota: false, updated_at: "2026-01-01T00:00:00Z".into(), - error: None, - error_state: codexbar::core::ProviderStateKind::Ready, - pace: None, - account_organization: None, - tray_status_label: None, - fetch_duration_ms: None, - wayfinder_usage: None, - quota_burndown: None, - open_ai_api_usage: None, - session_equivalent_forecast: None, + ..Default::default() } } diff --git a/apps/desktop-tauri/src-tauri/src/powertoys.rs b/apps/desktop-tauri/src-tauri/src/powertoys.rs index 305abe3dd9b..0efb3b64637 100644 --- a/apps/desktop-tauri/src-tauri/src/powertoys.rs +++ b/apps/desktop-tauri/src-tauri/src/powertoys.rs @@ -173,17 +173,7 @@ mod tests { RateWindowSnapshot { used_percent, remaining_percent: 100.0 - used_percent, - window_minutes: None, - resets_at: None, - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, - reserve_will_last_to_reset: false, + ..Default::default() } } @@ -194,31 +184,12 @@ mod tests { display_name: "Test Provider".to_string(), primary: rate_window(42.0), primary_label: Some("Session".to_string()), - secondary: None, - secondary_label: None, - model_specific: None, - tertiary: None, - tertiary_label: None, - extra_rate_windows: Vec::new(), - inventory: Vec::new(), - display_details: Vec::new(), - cost: None, plan_name: Some("Team".to_string()), account_email: Some("dev@example.com".to_string()), - subscription: None, source_label: "web".to_string(), - has_successful_claude_cli_quota: false, updated_at: "2026-07-09T00:00:00Z".to_string(), - error: None, - error_state: codexbar::core::ProviderStateKind::Ready, - pace: None, account_organization: Some("Example Org".to_string()), - tray_status_label: None, - fetch_duration_ms: None, - wayfinder_usage: None, - quota_burndown: None, - open_ai_api_usage: None, - session_equivalent_forecast: None, + ..Default::default() }); let value = serde_json::to_value(snapshot).unwrap(); @@ -251,31 +222,9 @@ mod tests { display_name: "Test Provider".to_string(), primary: rate_window(42.0), primary_label: Some("Session".to_string()), - secondary: None, - secondary_label: None, - model_specific: None, - tertiary: None, - tertiary_label: None, - extra_rate_windows: Vec::new(), - inventory: Vec::new(), - display_details: Vec::new(), - cost: None, - plan_name: None, - account_email: None, - subscription: None, source_label: "web".to_string(), - has_successful_claude_cli_quota: false, updated_at: "2026-07-09T00:00:00Z".to_string(), - error: None, - error_state: codexbar::core::ProviderStateKind::Ready, - pace: None, - account_organization: None, - tray_status_label: None, - fetch_duration_ms: None, - wayfinder_usage: None, - open_ai_api_usage: None, - session_equivalent_forecast: None, - quota_burndown: None, + ..Default::default() }); let value = serde_json::to_value(snapshot).unwrap(); diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs index ce7e8bf1b22..5fcd96d5f01 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs @@ -687,55 +687,18 @@ mod tests { primary: crate::commands::RateWindowSnapshot { used_percent, remaining_percent: 100.0 - used_percent, - window_minutes: None, - resets_at: None, - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() }, - primary_label: None, secondary: secondary_percent.map(|pct| crate::commands::RateWindowSnapshot { used_percent: pct, remaining_percent: 100.0 - pct, - window_minutes: None, - resets_at: None, - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() }), - secondary_label: None, - model_specific: None, tertiary: tertiary_percent.map(|pct| crate::commands::RateWindowSnapshot { used_percent: pct, remaining_percent: 100.0 - pct, - window_minutes: None, - resets_at: None, - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() }), - tertiary_label: None, - extra_rate_windows: Vec::new(), - inventory: Vec::new(), - display_details: Vec::new(), cost: cost.map(|(used, limit)| crate::commands::CostSnapshotBridge { used, limit: Some(limit), @@ -753,22 +716,8 @@ mod tests { daily: Vec::new(), always_visible: false, }), - plan_name: None, - account_email: None, - subscription: None, - source_label: String::new(), - has_successful_claude_cli_quota: false, updated_at: "2025-01-01T00:00:00Z".into(), - error: None, - error_state: codexbar::core::ProviderStateKind::Ready, - pace: None, - account_organization: None, - tray_status_label: None, - fetch_duration_ms: None, - wayfinder_usage: None, - quota_burndown: None, - open_ai_api_usage: None, - session_equivalent_forecast: None, + ..Default::default() } } @@ -789,17 +738,7 @@ mod tests { window: crate::commands::RateWindowSnapshot { used_percent: percent, remaining_percent: 100.0 - percent, - window_minutes: None, - resets_at: None, - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - description_is_detail: false, - monthly_limit_block: None, + ..Default::default() }, } } @@ -1091,16 +1030,8 @@ mod tests { used_percent: 100.0, remaining_percent: 0.0, window_minutes: Some(10080), - resets_at: None, - reset_description: None, is_exhausted: true, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - description_is_detail: false, - monthly_limit_block: None, + ..Default::default() }); let (primary, _) = selected_tray_percents(&snapshot, &settings); assert_eq!(primary, 22.0); @@ -1130,17 +1061,7 @@ mod tests { snapshot.model_specific = Some(crate::commands::RateWindowSnapshot { used_percent: 55.0, remaining_percent: 45.0, - window_minutes: None, - resets_at: None, - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - description_is_detail: false, - monthly_limit_block: None, + ..Default::default() }); snapshot.extra_rate_windows.push(fake_extra_window(90.0)); let (primary, _) = selected_tray_percents(&snapshot, &settings); diff --git a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs index e443735870a..3346150db43 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs @@ -1,5 +1,5 @@ use super::*; -use codexbar::core::{ProviderId, ProviderStateKind}; +use codexbar::core::ProviderId; fn fake_snapshot(id: &str, display_name: &str, used_percent: f64) -> ProviderUsageSnapshot { fake_snapshot_with(id, display_name, used_percent, None, None, None) @@ -16,32 +16,15 @@ fn fake_snapshot_with( let window = |percent: f64| RateWindowSnapshot { used_percent: percent, remaining_percent: 100.0 - percent, - window_minutes: None, - resets_at: None, - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() }; ProviderUsageSnapshot { provider_id: id.into(), display_name: display_name.into(), primary: window(used_percent), - primary_label: None, secondary: secondary_percent.map(window), - secondary_label: None, - model_specific: None, tertiary: tertiary_percent.map(window), - tertiary_label: None, - extra_rate_windows: Vec::new(), - inventory: Vec::new(), - display_details: Vec::new(), cost: cost.map(|(used, limit)| crate::commands::CostSnapshotBridge { used, limit: Some(limit), @@ -59,22 +42,8 @@ fn fake_snapshot_with( daily: Vec::new(), always_visible: false, }), - plan_name: None, - account_email: None, - subscription: None, - source_label: String::new(), - has_successful_claude_cli_quota: false, updated_at: "2025-01-01T00:00:00Z".into(), - error: None, - error_state: ProviderStateKind::Ready, - pace: None, - account_organization: None, - open_ai_api_usage: None, - tray_status_label: None, - fetch_duration_ms: None, - wayfinder_usage: None, - session_equivalent_forecast: None, - quota_burndown: None, + ..Default::default() } } @@ -371,17 +340,7 @@ fn fake_extra_window(percent: f64) -> crate::commands::NamedRateWindowSnapshot { window: crate::commands::RateWindowSnapshot { used_percent: percent, remaining_percent: 100.0 - percent, - window_minutes: None, - resets_at: None, - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() }, } } @@ -595,16 +554,8 @@ fn claude_automatic_prefers_weekly_when_model_exhausted() { used_percent: 100.0, remaining_percent: 0.0, window_minutes: Some(10080), - resets_at: None, - reset_description: None, is_exhausted: true, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() }); let (primary, _) = selected_tray_percents(&snapshot, &settings); @@ -639,17 +590,7 @@ fn automatic_picks_highest_among_model_and_extra_windows() { snapshot.model_specific = Some(crate::commands::RateWindowSnapshot { used_percent: 55.0, remaining_percent: 45.0, - window_minutes: None, - resets_at: None, - reset_description: None, - is_exhausted: false, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() }); snapshot.extra_rate_windows.push(fake_extra_window(90.0)); diff --git a/apps/desktop-tauri/src-tauri/src/usage_metric.rs b/apps/desktop-tauri/src-tauri/src/usage_metric.rs index cab5254a13e..78eac02a122 100644 --- a/apps/desktop-tauri/src-tauri/src/usage_metric.rs +++ b/apps/desktop-tauri/src-tauri/src/usage_metric.rs @@ -392,17 +392,9 @@ fn derived_window(used_percent: f64, resets_at: Option) -> RateWindowSna RateWindowSnapshot { used_percent, remaining_percent: 100.0 - used_percent, - window_minutes: None, resets_at, - reset_description: None, is_exhausted: used_percent >= 100.0, - is_informational: false, - reserve_percent: None, - reserve_description: None, - reserve_will_last_to_reset: false, - reserve_eta_seconds: None, - monthly_limit_block: None, - description_is_detail: false, + ..Default::default() } } @@ -465,32 +457,10 @@ mod tests { provider_id: "codex".to_string(), display_name: "Codex".to_string(), primary: window(20.0), - primary_label: None, secondary: Some(window(60.0)), - secondary_label: None, - model_specific: None, - tertiary: None, - tertiary_label: None, - extra_rate_windows: Vec::new(), - inventory: Vec::new(), - display_details: Vec::new(), - cost: None, - plan_name: None, - account_email: None, - subscription: None, source_label: "test".to_string(), - has_successful_claude_cli_quota: false, updated_at: "2026-08-16T00:00:00Z".to_string(), - error: None, - error_state: codexbar::core::ProviderStateKind::Ready, - pace: None, - account_organization: None, - tray_status_label: None, - fetch_duration_ms: None, - wayfinder_usage: None, - quota_burndown: None, - open_ai_api_usage: None, - session_equivalent_forecast: None, + ..Default::default() } } From 4813f5b667770898892a845613c8edcbf580fcfb Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:45:34 +0700 Subject: [PATCH 02/22] Drop tray_bridge tests duplicated in tray_presentation_tests --- .../src-tauri/src/tray_accounts.rs | 8 +- .../src-tauri/src/tray_bridge.rs | 388 +----------------- apps/desktop-tauri/src-tauri/src/tray_menu.rs | 12 +- .../src-tauri/src/tray_presentation.rs | 2 +- .../src-tauri/src/tray_presentation_tests.rs | 32 +- 5 files changed, 43 insertions(+), 399 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/tray_accounts.rs b/apps/desktop-tauri/src-tauri/src/tray_accounts.rs index eb9c897e387..c90d523efd0 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_accounts.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_accounts.rs @@ -371,13 +371,7 @@ fn grok_accounts_menu( #[cfg(test)] mod tests { use super::*; - - fn menu_contains(menu: &[TrayMenuEntry], id: &str) -> bool { - menu.iter().any(|entry| { - entry.id.as_deref() == Some(id) - || (!entry.children.is_empty() && menu_contains(&entry.children, id)) - }) - } + use crate::tray_menu::tests::menu_contains; #[test] fn account_action_ids_are_typed_and_validated() { diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs index 5fcd96d5f01..11dfc7d1758 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs @@ -11,8 +11,6 @@ use tauri::{AppHandle, Manager}; use crate::shell; use crate::state::{AppState, TrayAnchor}; -#[cfg(test)] -use crate::tray_menu::build_tray_menu; use crate::tray_menu::{MenuState, TrayMenuEntry, build_tray_menu_with}; use crate::tray_presentation::{TrayPresentationPlan, headline_window}; @@ -454,17 +452,6 @@ fn truncate_tooltip_text(text: &str, max_chars: usize) -> String { } } -#[allow( - dead_code, - reason = "tray bridge helper reserved for future system tray integration" -)] -fn menu_contains(menu: &[TrayMenuEntry], id: &str) -> bool { - menu.iter().any(|entry| { - entry.id.as_deref() == Some(id) - || (!entry.children.is_empty() && menu_contains(&entry.children, id)) - }) -} - enum NativeMenuEntry { Item(MenuItem), CheckItem(tauri::menu::CheckMenuItem), @@ -536,40 +523,7 @@ mod tests { use super::*; use crate::surface::SurfaceMode; - use crate::tray_presentation::{ - codex_lane_headline_window, pick_tray_provider, selected_tray_percents, - }; - use codexbar::core::ProviderId; - use codexbar::settings::MetricPreference; - - fn sample_provider_catalog() -> Vec { - vec![ - ProviderCatalogEntry { - id: "codex".into(), - display_name: "Codex".into(), - cookie_domain: None, - }, - ProviderCatalogEntry { - id: "claude".into(), - display_name: "Claude".into(), - cookie_domain: None, - }, - ] - } - - #[test] - fn tray_menu_includes_about_and_provider_entries() { - let menu = build_tray_menu( - &sample_provider_catalog(), - &[], - &["codex".to_string(), "claude".to_string()] - .into_iter() - .collect(), - ); - assert!(menu_contains(&menu, "about")); - assert!(menu_contains(&menu, "toggle_provider:codex")); - assert!(menu_contains(&menu, "quit")); - } + use crate::tray_presentation::tests::{fake_snapshot, fake_snapshot_with}; #[test] fn toggle_float_bar_routes_to_toggle_action() { @@ -673,100 +627,6 @@ mod tests { assert!(anchor.is_none()); } - fn fake_snapshot_with( - id: &str, - display: &str, - used_percent: f64, - secondary_percent: Option, - tertiary_percent: Option, - cost: Option<(f64, f64)>, - ) -> crate::commands::ProviderUsageSnapshot { - crate::commands::ProviderUsageSnapshot { - provider_id: id.into(), - display_name: display.into(), - primary: crate::commands::RateWindowSnapshot { - used_percent, - remaining_percent: 100.0 - used_percent, - ..Default::default() - }, - secondary: secondary_percent.map(|pct| crate::commands::RateWindowSnapshot { - used_percent: pct, - remaining_percent: 100.0 - pct, - ..Default::default() - }), - tertiary: tertiary_percent.map(|pct| crate::commands::RateWindowSnapshot { - used_percent: pct, - remaining_percent: 100.0 - pct, - ..Default::default() - }), - cost: cost.map(|(used, limit)| crate::commands::CostSnapshotBridge { - used, - limit: Some(limit), - remaining: Some((limit - used).max(0.0)), - currency_code: "USD".to_string(), - currency_symbol: None, - period: "monthly".to_string(), - resets_at: None, - formatted_used: format!("${used:.2}"), - formatted_limit: Some(format!("${limit:.2}")), - balance: None, - formatted_balance: None, - balance_updated_at: None, - account_id: None, - daily: Vec::new(), - always_visible: false, - }), - updated_at: "2025-01-01T00:00:00Z".into(), - ..Default::default() - } - } - - fn fake_snapshot( - id: &str, - display: &str, - used_percent: f64, - ) -> crate::commands::ProviderUsageSnapshot { - fake_snapshot_with(id, display, used_percent, None, None, None) - } - - fn fake_extra_window(percent: f64) -> crate::commands::NamedRateWindowSnapshot { - crate::commands::NamedRateWindowSnapshot { - id: "additional_budget".to_string(), - title: "Additional Budget".to_string(), - fallback_lane: false, - icon_fallback: None, - window: crate::commands::RateWindowSnapshot { - used_percent: percent, - remaining_percent: 100.0 - percent, - ..Default::default() - }, - } - } - #[test] - fn pick_tray_provider_highest_picks_max_primary() { - let a = fake_snapshot("codex", "Codex", 30.0); - let b = fake_snapshot("claude", "Claude", 72.5); - let c = fake_snapshot("gemini", "Gemini", 50.0); - let refs: Vec<&crate::commands::ProviderUsageSnapshot> = vec![&a, &b, &c]; - let picked = pick_tray_provider(&refs, /* prefer_highest = */ true) - .expect("highest mode should pick a provider"); - assert_eq!(picked.provider_id, "claude"); - } - #[test] - fn pick_tray_provider_first_preserves_catalog_order() { - let a = fake_snapshot("codex", "Codex", 30.0); - let b = fake_snapshot("claude", "Claude", 72.5); - let refs: Vec<&crate::commands::ProviderUsageSnapshot> = vec![&a, &b]; - let picked = pick_tray_provider(&refs, /* prefer_highest = */ false) - .expect("non-highest mode should still pick the first entry"); - assert_eq!(picked.provider_id, "codex"); - } - #[test] - fn pick_tray_provider_none_when_empty() { - let refs: Vec<&crate::commands::ProviderUsageSnapshot> = vec![]; - assert!(pick_tray_provider(&refs, true).is_none()); - assert!(pick_tray_provider(&refs, false).is_none()); - } #[test] fn tooltip_uses_compact_status_labels() { let mut claude = fake_snapshot("claude", "Claude", 13.0); @@ -856,250 +716,4 @@ mod tests { assert!(english_label.contains("Resets in"), "{english_label}"); assert!(japanese_label.contains("リセットまで"), "{japanese_label}"); } - #[test] - fn selected_tray_percent_uses_cursor_extra_usage_cost() { - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Cursor, MetricPreference::ExtraUsage); - let snapshot = fake_snapshot_with( - "cursor", - "Cursor", - 10.0, - Some(20.0), - Some(72.0), - Some((15.0, 100.0)), - ); - let (primary, secondary) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 15.0); - assert_eq!(secondary, Some(20.0)); - } - #[test] - fn selected_tray_percent_tracks_extra_rate_window() { - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Copilot, MetricPreference::ExtraUsage); - let mut snapshot = fake_snapshot("copilot", "Copilot", 20.0); - snapshot.extra_rate_windows.push(fake_extra_window(42.0)); - let (primary, secondary) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 42.0); - assert_eq!(secondary, None); - } - #[test] - fn copilot_automatic_tracks_highest_extra_rate_window() { - let settings = Settings::default(); - let mut snapshot = fake_snapshot("copilot", "Copilot", 20.0); - snapshot.extra_rate_windows.push(fake_extra_window(42.0)); - let (primary, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 42.0); - } - #[test] - fn selected_tray_percent_respects_remaining_display_mode() { - let mut settings = Settings { - show_as_used: false, - ..Settings::default() - }; - settings.set_provider_metric(ProviderId::Cursor, MetricPreference::ExtraUsage); - let snapshot = fake_snapshot_with( - "cursor", - "Cursor", - 10.0, - Some(20.0), - Some(72.0), - Some((15.0, 100.0)), - ); - let (primary, secondary) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 85.0); - assert_eq!(secondary, Some(80.0)); - } - #[test] - fn exhausted_automatic_window_never_renders_as_remaining_progress() { - let mut settings = Settings { - show_as_used: false, - ..Settings::default() - }; - let mut snapshot = fake_snapshot_with( - "opencodego", - "OpenCode Go", - 20.0, - Some(60.0), - Some(40.0), - None, - ); - snapshot - .tertiary - .as_mut() - .expect("monthly quota") - .is_exhausted = true; - let (remaining, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(remaining, 0.0); - settings.show_as_used = true; - let (used, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(used, 100.0); - } - #[test] - fn full_automatic_window_without_exhausted_flag_has_zero_remaining_progress() { - let mut settings = Settings { - show_as_used: false, - ..Settings::default() - }; - let mut snapshot = fake_snapshot_with( - "opencodego", - "OpenCode Go", - 20.0, - Some(60.0), - Some(100.0), - None, - ); - snapshot - .tertiary - .as_mut() - .expect("monthly quota") - .is_exhausted = false; - let (remaining, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(remaining, 0.0); - settings.show_as_used = true; - let (used, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(used, 100.0); - } - #[test] - fn missing_automatic_window_does_not_look_like_available_remaining_progress() { - let settings = Settings { - show_as_used: false, - ..Settings::default() - }; - let mut snapshot = fake_snapshot_with("opencodego", "OpenCode Go", 0.0, None, None, None); - snapshot.primary.is_informational = true; - let (remaining, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(remaining, 0.0); - } - #[test] - fn selected_tray_percent_falls_back_when_extra_usage_missing() { - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Cursor, MetricPreference::ExtraUsage); - let snapshot = fake_snapshot_with("cursor", "Cursor", 10.0, Some(72.0), None, None); - let (primary, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 72.0); - } - #[test] - fn single_meaningful_secondary_quota_uses_full_single_meter() { - let settings = Settings::default(); - let mut snapshot = fake_snapshot_with("claude", "Claude", 0.0, Some(42.0), None, None); - snapshot.primary.is_informational = true; - let (primary, secondary) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 42.0); - assert_eq!(secondary, None); - } - #[test] - fn selected_secondary_quota_is_not_duplicated_when_tertiary_is_meaningful() { - let settings = Settings::default(); - let mut snapshot = - fake_snapshot_with("claude", "Claude", 0.0, Some(42.0), Some(30.0), None); - snapshot.primary.is_informational = true; - let (primary, secondary) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 42.0); - assert_eq!(secondary, Some(30.0)); - } - #[test] - fn two_meaningful_quotas_keep_two_meter_layout() { - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Cursor, MetricPreference::Session); - let snapshot = fake_snapshot_with("cursor", "Cursor", 15.0, Some(40.0), None, None); - let (primary, secondary) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 15.0); - assert_eq!(secondary, Some(40.0)); - } - #[test] - fn informational_primary_skips_session_and_automatic_phantom_zero() { - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Claude, MetricPreference::Session); - let mut snapshot = fake_snapshot_with("claude", "Claude", 0.0, Some(42.0), None, None); - snapshot.primary.is_informational = true; - // Session preference must not paint the synthetic 0% primary; - // it falls through to Automatic which prefers weekly (42%). - let (primary, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 42.0); - assert_ne!(primary, 0.0); - // Automatic also prefers weekly over informational primary. - settings.set_provider_metric(ProviderId::Claude, MetricPreference::Automatic); - let (primary, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 42.0); - } - #[test] - fn claude_automatic_prefers_weekly_when_model_exhausted() { - let settings = Settings::default(); - let mut snapshot = fake_snapshot_with("claude", "Claude", 40.0, Some(22.0), None, None); - snapshot.model_specific = Some(crate::commands::RateWindowSnapshot { - used_percent: 100.0, - remaining_percent: 0.0, - window_minutes: Some(10080), - is_exhausted: true, - ..Default::default() - }); - let (primary, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 22.0); - // Explicit model override is untouched. - let mut overridden = settings.clone(); - overridden.set_provider_metric(ProviderId::Claude, MetricPreference::Model); - let (primary, _) = selected_tray_percents(&snapshot, &overridden); - assert_eq!(primary, 100.0); - } - #[test] - fn automatic_prefers_exhausted_weekly_over_low_session() { - let settings = Settings::default(); - let snapshot = fake_snapshot_with("codex", "Codex", 20.0, Some(100.0), None, None); - let (primary, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 100.0); - // Explicit session override still wins. - let mut overridden = settings.clone(); - overridden.set_provider_metric(ProviderId::Codex, MetricPreference::Session); - let (primary, _) = selected_tray_percents(&snapshot, &overridden); - assert_eq!(primary, 20.0); - } - #[test] - fn automatic_picks_highest_among_model_and_extra_windows() { - let settings = Settings::default(); - let mut snapshot = - fake_snapshot_with("gemini", "Gemini", 10.0, Some(30.0), Some(40.0), None); - snapshot.model_specific = Some(crate::commands::RateWindowSnapshot { - used_percent: 55.0, - remaining_percent: 45.0, - ..Default::default() - }); - snapshot.extra_rate_windows.push(fake_extra_window(90.0)); - let (primary, _) = selected_tray_percents(&snapshot, &settings); - assert_eq!(primary, 90.0); - } - #[test] - fn f5_headline_prefers_non_informational_primary() { - let snapshot = fake_snapshot_with("codex", "Codex", 50.0, Some(20.0), Some(30.0), None); - let headline = codex_lane_headline_window(&snapshot); - assert!((headline.used_percent - 50.0).abs() < f64::EPSILON); - } - #[test] - fn f5_headline_falls_back_to_secondary_when_primary_informational() { - let mut snapshot = fake_snapshot_with("codex", "Codex", 0.0, Some(25.0), Some(30.0), None); - snapshot.primary.is_informational = true; - let headline = codex_lane_headline_window(&snapshot); - assert!((headline.used_percent - 25.0).abs() < f64::EPSILON); - } - #[test] - fn f5_headline_falls_back_to_tertiary_when_primary_and_secondary_informational() { - let mut snapshot = fake_snapshot_with("codex", "Codex", 0.0, Some(0.0), Some(35.0), None); - snapshot.primary.is_informational = true; - snapshot.secondary.as_mut().unwrap().is_informational = true; - let headline = codex_lane_headline_window(&snapshot); - assert!((headline.used_percent - 35.0).abs() < f64::EPSILON); - } - #[test] - fn f5_headline_returns_primary_when_all_informational() { - let mut snapshot = fake_snapshot_with("codex", "Codex", 0.0, Some(0.0), Some(0.0), None); - snapshot.primary.is_informational = true; - if let Some(sec) = &mut snapshot.secondary { - sec.is_informational = true; - } - if let Some(ter) = &mut snapshot.tertiary { - ter.is_informational = true; - } - let headline = codex_lane_headline_window(&snapshot); - // Falls back to primary (the placeholder) when all are informational. - assert!(headline.is_informational); - } } diff --git a/apps/desktop-tauri/src-tauri/src/tray_menu.rs b/apps/desktop-tauri/src-tauri/src/tray_menu.rs index 3498739af8d..2df110eb41f 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_menu.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_menu.rs @@ -179,10 +179,10 @@ pub(crate) fn build_tray_menu_with( } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; - fn menu_contains(menu: &[TrayMenuEntry], id: &str) -> bool { + pub(crate) fn menu_contains(menu: &[TrayMenuEntry], id: &str) -> bool { menu.iter().any(|entry| { entry.id.as_deref() == Some(id) || (!entry.children.is_empty() && menu_contains(&entry.children, id)) @@ -210,6 +210,14 @@ mod tests { .collect() } + #[test] + fn tray_menu_includes_about_and_provider_entries() { + let menu = build_tray_menu(&sample_provider_catalog(), &[], &both_enabled()); + assert!(menu_contains(&menu, "about")); + assert!(menu_contains(&menu, "toggle_provider:codex")); + assert!(menu_contains(&menu, "quit")); + } + #[test] fn check_for_updates_item_is_present() { let menu = build_tray_menu(&sample_provider_catalog(), &[], &both_enabled()); diff --git a/apps/desktop-tauri/src-tauri/src/tray_presentation.rs b/apps/desktop-tauri/src-tauri/src/tray_presentation.rs index 2dadce07348..894e6f9012d 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation.rs @@ -412,4 +412,4 @@ fn display_metric_percent(window: &RateWindowSnapshot, show_as_used: bool) -> f6 #[cfg(test)] #[path = "tray_presentation_tests.rs"] -mod tests; +pub(crate) mod tests; diff --git a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs index 3346150db43..f7d6247d2ca 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs @@ -1,11 +1,11 @@ use super::*; use codexbar::core::ProviderId; -fn fake_snapshot(id: &str, display_name: &str, used_percent: f64) -> ProviderUsageSnapshot { +pub(crate) fn fake_snapshot(id: &str, display_name: &str, used_percent: f64) -> ProviderUsageSnapshot { fake_snapshot_with(id, display_name, used_percent, None, None, None) } -fn fake_snapshot_with( +pub(crate) fn fake_snapshot_with( id: &str, display_name: &str, used_percent: f64, @@ -331,6 +331,34 @@ fn codex_headline_skips_informational_primary() { assert_eq!(codex_lane_headline_window(&snapshot).used_percent, 25.0); } +#[test] +fn pick_tray_provider_highest_picks_max_primary() { + let a = fake_snapshot("codex", "Codex", 30.0); + let b = fake_snapshot("claude", "Claude", 72.5); + let c = fake_snapshot("gemini", "Gemini", 50.0); + let refs: Vec<&ProviderUsageSnapshot> = vec![&a, &b, &c]; + let picked = pick_tray_provider(&refs, /* prefer_highest = */ true) + .expect("highest mode should pick a provider"); + assert_eq!(picked.provider_id, "claude"); +} + +#[test] +fn pick_tray_provider_first_preserves_catalog_order() { + let a = fake_snapshot("codex", "Codex", 30.0); + let b = fake_snapshot("claude", "Claude", 72.5); + let refs: Vec<&ProviderUsageSnapshot> = vec![&a, &b]; + let picked = pick_tray_provider(&refs, /* prefer_highest = */ false) + .expect("non-highest mode should still pick the first entry"); + assert_eq!(picked.provider_id, "codex"); +} + +#[test] +fn pick_tray_provider_none_when_empty() { + let refs: Vec<&ProviderUsageSnapshot> = vec![]; + assert!(pick_tray_provider(&refs, true).is_none()); + assert!(pick_tray_provider(&refs, false).is_none()); +} + fn fake_extra_window(percent: f64) -> crate::commands::NamedRateWindowSnapshot { crate::commands::NamedRateWindowSnapshot { id: "additional_budget".to_string(), From 5d48192da0ff5878d7d8d4aeb12cd7fcaf58adeb Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:54:29 +0700 Subject: [PATCH 03/22] Simplify the provider refresh engine and share RFC 3339 parsing --- .../src-tauri/src/commands/bridge.rs | 20 + .../src-tauri/src/commands/bridge/status.rs | 4 +- .../src-tauri/src/commands/chart.rs | 20 +- .../src-tauri/src/commands/codex_accounts.rs | 7 +- .../src-tauri/src/commands/providers.rs | 344 +++++------------- .../src/commands/providers/reset_backfill.rs | 2 +- .../src-tauri/src/tray_presentation_tests.rs | 6 +- 7 files changed, 119 insertions(+), 284 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index dea4cf29c59..1b0f18d93ed 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -59,7 +59,27 @@ impl Default for RateWindowSnapshot { } } +/// Parse an RFC 3339 bridge timestamp into UTC. +pub(crate) fn parse_utc(value: &str) -> Option> { + chrono::DateTime::parse_from_rfc3339(value) + .ok() + .map(|dt| dt.with_timezone(&chrono::Utc)) +} + impl RateWindowSnapshot { + pub(crate) fn resets_at_utc(&self) -> Option> { + self.resets_at.as_deref().and_then(parse_utc) + } + + pub(crate) fn to_rate_window(&self) -> RateWindow { + RateWindow::with_details( + self.used_percent, + self.window_minutes, + self.resets_at_utc(), + self.reset_description.clone(), + ) + } + pub(super) fn from_rate_window(rw: &RateWindow) -> Self { Self { used_percent: rw.used_percent, diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge/status.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge/status.rs index 130c2762ad8..9ffa2bfbc33 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge/status.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge/status.rs @@ -26,9 +26,7 @@ fn compact_reset_description( lang: codexbar::settings::Language, ) -> Option { if let Some(ref resets_at) = window.resets_at { - let dt = chrono::DateTime::parse_from_rfc3339(resets_at) - .ok() - .map(|dt| dt.with_timezone(&chrono::Utc))?; + let dt = super::parse_utc(resets_at)?; return Some(format_compact_reset_countdown(dt, lang)); } diff --git a/apps/desktop-tauri/src-tauri/src/commands/chart.rs b/apps/desktop-tauri/src-tauri/src/commands/chart.rs index c2c37fc7b77..426406de2ad 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/chart.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/chart.rs @@ -8,7 +8,7 @@ use crate::commands::bridge::RateWindowSnapshot; use crate::state::AppState; use chrono::{DateTime, Utc}; -use codexbar::core::{OpenAIDashboardCacheStore, RateWindow}; +use codexbar::core::OpenAIDashboardCacheStore; use codexbar::cost_reporting_period::{CostReportingPeriod, CostTimeZone}; use codexbar::cost_scanner::{ CostScanner, CostSummary, TodayUsage, get_daily_cost_history, get_daily_token_history, @@ -189,7 +189,9 @@ fn build_provider_chart_data_with_cancel( cancel: Option>, weekly_window: Option, ) -> ProviderChartData { - let live_window = weekly_window.as_ref().map(rate_window_from_snapshot); + let live_window = weekly_window + .as_ref() + .map(RateWindowSnapshot::to_rate_window); let provider_snapshot = codexbar::providers::chart::build_chart_snapshot( &provider_id, account_email.as_deref(), @@ -428,20 +430,6 @@ fn local_usage_summary_from_cost_summary( }) } -fn rate_window_from_snapshot(snapshot: &RateWindowSnapshot) -> RateWindow { - let resets_at = snapshot - .resets_at - .as_deref() - .and_then(|value| DateTime::parse_from_rfc3339(value).ok()) - .map(|value| value.with_timezone(&Utc)); - RateWindow::with_details( - snapshot.used_percent, - snapshot.window_minutes, - resets_at, - snapshot.reset_description.clone(), - ) -} - fn active_chart_scans() -> &'static Mutex>> { static ACTIVE: OnceLock>>> = OnceLock::new(); ACTIVE.get_or_init(|| Mutex::new(HashMap::new())) diff --git a/apps/desktop-tauri/src-tauri/src/commands/codex_accounts.rs b/apps/desktop-tauri/src-tauri/src/commands/codex_accounts.rs index 66d3fe9d319..d568cdc2461 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/codex_accounts.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/codex_accounts.rs @@ -14,7 +14,7 @@ use codexbar::settings::Settings; use crate::state::AppState; use super::credential_alerts::FetchAttempt; -use super::providers::quota_notification_account_identity_for; +use super::warning_identity::WarningIdentity; use super::*; // ── Codex multi-account (ADR 0003, milestone 2) ────────────────────── @@ -252,13 +252,14 @@ fn observe_codex_account_lane_outcome( return; } - let account_scope = quota_notification_account_identity_for( + let account_scope = WarningIdentity::new( ProviderId::Codex, "", account.email_hint.as_deref(), None, Some(account.id), - ); + ) + .threshold_key(); match attempt { FetchAttempt::Succeeded => { manager.observe_credential_recovery(ProviderId::Codex, &account_scope); diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers.rs b/apps/desktop-tauri/src-tauri/src/commands/providers.rs index 8d6999d63d5..bcac222bb30 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers.rs @@ -543,7 +543,7 @@ pub(crate) fn prune_provider_cache_to_enabled( /// Invalidate in-flight publish work and remove disabled providers from cache. /// /// Also clears the refresh lock so a follow-up force refresh can start immediately -/// (otherwise `begin_provider_refresh` no-ops while a superseded batch still holds +/// (otherwise `reserve_provider_refresh` no-ops while a superseded batch still holds /// `is_refreshing`, and newly enabled providers never get a replacement run). pub(crate) fn invalidate_provider_refresh_and_prune_disabled( state: &tauri::State<'_, Mutex>, @@ -625,8 +625,13 @@ async fn do_refresh_providers_with_policy( } let inputs = ProviderRefreshInputs::load(settings, enabled_ids); - let generation = match begin_provider_refresh(&state, force, &refresh_ids, expected_generation)? - { + let reservation = reserve_provider_refresh( + &mut *state.lock().map_err(|e| e.to_string())?, + force, + &refresh_ids, + expected_generation, + ); + let generation = match reservation { ProviderRefreshReservation::Reserved { generation } => generation, ProviderRefreshReservation::Skipped(reason) => { // Settings or account identity changed while inputs were loading, @@ -659,9 +664,13 @@ async fn do_refresh_providers_with_policy( generation, scope.refresh_account_lanes(), ); - await_provider_refreshes(handles).await; + for handle in handles { + let _ = handle.await; + } - let error_count = match finish_provider_refresh(&state, generation)? { + let completion = + complete_provider_refresh(&mut *state.lock().map_err(|e| e.to_string())?, generation); + let error_count = match completion { ProviderRefreshCompletion::Published { error_count } => error_count, ProviderRefreshCompletion::Superseded { current_generation } => { // Superseded by a newer generation (or invalidate). Do not clear UI @@ -680,21 +689,6 @@ async fn do_refresh_providers_with_policy( Ok(ProviderRefreshOutcome::Published { generation }) } -fn begin_provider_refresh( - state: &tauri::State<'_, Mutex>, - force: bool, - provider_ids: &[ProviderId], - expected_generation: u64, -) -> Result { - let mut guard = state.lock().map_err(|e| e.to_string())?; - Ok(reserve_provider_refresh( - &mut guard, - force, - provider_ids, - expected_generation, - )) -} - struct ProviderRefreshInputs { settings: Settings, enabled_ids: Vec, @@ -982,19 +976,15 @@ fn preserve_last_good_transient_failure_with_policy( policy: Option, ownership: &codexbar::core::FailureOwnership, ) -> ProviderUsageSnapshot { - let Some(error) = snapshot.error.as_deref() else { - guard.transient_provider_failure_counts.remove(&id); - return snapshot; - }; + use codexbar::core::LastGoodFailurePolicy as Policy; - let policy = policy.unwrap_or(codexbar::core::LastGoodFailurePolicy::Replace); - if policy == codexbar::core::LastGoodFailurePolicy::Replace { - guard.transient_provider_failure_counts.remove(&id); - return snapshot; - } + let policy = policy.unwrap_or(Policy::Replace); // A failure tied to a live session may keep only a snapshot that the same // session produced. Anything else shows the error. - if !ownership.allows_retention(guard.last_good_owners.get(&id)) { + if snapshot.error.is_none() + || policy == Policy::Replace + || !ownership.allows_retention(guard.last_good_owners.get(&id)) + { guard.transient_provider_failure_counts.remove(&id); return snapshot; } @@ -1015,47 +1005,33 @@ fn preserve_last_good_transient_failure_with_policy( .transient_provider_failure_counts .entry(id) .or_insert(0); - match policy { - codexbar::core::LastGoodFailurePolicy::Preserve => { - tracing::warn!( - provider = id.cli_name(), - error, - "preserving last good provider snapshot after transient failure" - ); - previous - } - codexbar::core::LastGoodFailurePolicy::PreserveOnce if *count == 0 => { + let preserve = match policy { + Policy::Preserve => true, + Policy::PreserveOnce | Policy::PreserveOnceThenSurface if *count == 0 => { *count = 1; - tracing::warn!( - provider = id.cli_name(), - error, - "preserving last good provider snapshot after transient failure" - ); - previous - } - codexbar::core::LastGoodFailurePolicy::PreserveOnce => { - *count = count.saturating_add(1); - snapshot - } - codexbar::core::LastGoodFailurePolicy::PreserveOnceThenSurface if *count == 0 => { - *count = 1; - tracing::warn!( - provider = id.cli_name(), - error, - "preserving last good provider snapshot after transient failure" - ); - previous + true } - codexbar::core::LastGoodFailurePolicy::PreserveOnceThenSurface => { + Policy::PreserveOnce | Policy::PreserveOnceThenSurface => { *count = count.saturating_add(1); - let mut surfaced = previous; - surfaced.error = snapshot.error; - surfaced.error_state = snapshot.error_state; - surfaced.fetch_duration_ms = snapshot.fetch_duration_ms; - surfaced + false } - codexbar::core::LastGoodFailurePolicy::Replace => snapshot, + Policy::Replace => false, + }; + if preserve { + tracing::warn!( + provider = id.cli_name(), + error = snapshot.error.as_deref().unwrap_or_default(), + "preserving last good provider snapshot after transient failure" + ); + return previous; + } + if policy == Policy::PreserveOnceThenSurface { + previous.error = snapshot.error; + previous.error_state = snapshot.error_state; + previous.fetch_duration_ms = snapshot.fetch_duration_ms; + return previous; } + snapshot } /// What a refresh tells the shell about keeping or replacing the last good @@ -1079,62 +1055,37 @@ async fn fetch_provider_snapshot( let metadata = provider.metadata().clone(); let started = std::time::Instant::now(); - let (mut snapshot, account_identity, retention) = - match tokio::time::timeout(provider_fetch_timeout(id, &ctx), provider.fetch_usage(&ctx)) + let outcome = + tokio::time::timeout(provider_fetch_timeout(id, &ctx), provider.fetch_usage(&ctx)) .await - { - Ok(Ok(result)) => { - let account_identity = result.account_identity().map(ToOwned::to_owned); - ( - ProviderUsageSnapshot::from_fetch_result( - id, - &metadata, - &result, - token_account_id, - ), - account_identity, - RefreshRetention { - fresh_owner: result.last_good_owner.clone(), - ..RefreshRetention::default() - }, - ) - } - Ok(Err(e)) => { - let policy = provider.last_good_failure_policy_for_error(&e); - ( - ProviderUsageSnapshot::from_error( - id, - &metadata, - codexbar::logging::safe_error_message(&e), - provider.error_state_kind(&e), - ), - None, - RefreshRetention { - policy: Some(policy), - failure_ownership: e.failure_ownership(), - fresh_owner: None, - }, - ) - } - Err(_) => { - let error = codexbar::core::ProviderError::Timeout; - let policy = provider.last_good_failure_policy_for_error(&error); - ( - ProviderUsageSnapshot::from_error( - id, - &metadata, - "Timeout".to_string(), - provider.error_state_kind(&error), - ), - None, - RefreshRetention { - policy: Some(policy), - failure_ownership: error.failure_ownership(), - fresh_owner: None, - }, - ) - } - }; + .unwrap_or_else(|_| Err(codexbar::core::ProviderError::Timeout)); + let (mut snapshot, account_identity, retention) = match outcome { + Ok(result) => { + let account_identity = result.account_identity().map(ToOwned::to_owned); + ( + ProviderUsageSnapshot::from_fetch_result(id, &metadata, &result, token_account_id), + account_identity, + RefreshRetention { + fresh_owner: result.last_good_owner.clone(), + ..RefreshRetention::default() + }, + ) + } + Err(e) => ( + ProviderUsageSnapshot::from_error( + id, + &metadata, + codexbar::logging::safe_error_message(&e), + provider.error_state_kind(&e), + ), + None, + RefreshRetention { + policy: Some(provider.last_good_failure_policy_for_error(&e)), + failure_ownership: e.failure_ownership(), + fresh_owner: None, + }, + ), + }; record_provider_fetch_duration(id, &mut snapshot, started); (snapshot, account_identity, retention) @@ -1156,23 +1107,6 @@ fn record_provider_fetch_duration( } } -async fn await_provider_refreshes(handles: Vec>) { - for handle in handles { - let _ = handle.await; - } -} - -/// Finish a refresh batch. Returns `None` when `generation` was superseded -/// (do not emit complete / tray updates for dead work). Returns `Some(error_count)` -/// when this batch still owns the generation and the lock was released. -fn finish_provider_refresh( - state: &tauri::State<'_, Mutex>, - generation: u64, -) -> Result { - let mut guard = state.lock().map_err(|e| e.to_string())?; - Ok(complete_provider_refresh(&mut guard, generation)) -} - fn update_tray_and_notifications( app: &tauri::AppHandle, state: &tauri::State<'_, Mutex>, @@ -1293,11 +1227,7 @@ fn resolve_toast_account( if lane.is_informational { return scope.key().to_string(); } - let resets_at = lane - .resets_at - .as_deref() - .and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok()) - .map(|date| date.with_timezone(&chrono::Utc)); + let resets_at = lane.resets_at_utc(); manager.resolve_warning_account( provider, scope, @@ -1343,34 +1273,18 @@ pub(super) fn quota_notification_account_identity( ) -> String { ProviderId::from_cli_name(&snapshot.provider_id) .map(|provider| { - quota_notification_account_identity_for( + WarningIdentity::new( provider, &snapshot.source_label, snapshot.account_email.as_deref(), snapshot.account_organization.as_deref(), token_account_id, ) + .threshold_key() }) .unwrap_or_default() } -pub(super) fn quota_notification_account_identity_for( - provider: ProviderId, - source_label: &str, - account_email: Option<&str>, - account_organization: Option<&str>, - token_account_id: Option, -) -> String { - WarningIdentity::new( - provider, - source_label, - account_email, - account_organization, - token_account_id, - ) - .threshold_key() -} - fn notify_predictive_pace( manager: &mut codexbar::notifications::NotificationManager, provider: ProviderId, @@ -1398,9 +1312,7 @@ fn notify_predictive_pace( let Some(identity) = warning_identity.predictive_key() else { return; }; - let observed_at = chrono::DateTime::parse_from_rfc3339(&snapshot.updated_at) - .ok() - .map(|date| date.with_timezone(&chrono::Utc)); + let observed_at = parse_utc(&snapshot.updated_at); for (warning_window, window, default_window_minutes) in [ ( @@ -1420,16 +1332,7 @@ fn notify_predictive_pace( if window.is_informational { continue; } - let rate_window = RateWindow::with_details( - window.used_percent, - window.window_minutes, - window - .resets_at - .as_deref() - .and_then(|value| chrono::DateTime::parse_from_rfc3339(value).ok()) - .map(|date| date.with_timezone(&chrono::Utc)), - window.reset_description.clone(), - ); + let rate_window = window.to_rate_window(); let Some(pace) = codexbar::core::UsagePace::weekly(&rate_window, observed_at, default_window_minutes) else { @@ -1600,93 +1503,14 @@ mod predictive_warning_tests { } } } -} -#[cfg(test)] -mod reset_backfill_tests { - use super::*; - use crate::commands::bridge::{ProviderUsageSnapshot, RateWindowSnapshot}; - fn win(used: f64, resets_at: Option<&str>) -> RateWindowSnapshot { - RateWindowSnapshot { - used_percent: used, - remaining_percent: 100.0 - used, - window_minutes: Some(300), - resets_at: resets_at.map(String::from), - ..Default::default() - } - } - fn codex_snapshot(primary: RateWindowSnapshot) -> ProviderUsageSnapshot { - ProviderUsageSnapshot { - provider_id: "codex".into(), - display_name: "Codex".into(), - primary, - updated_at: "2026-01-01T00:00:00Z".into(), - ..Default::default() - } - } - #[test] - fn f6_backfills_future_cached_reset() { - // Cached has a future resets_at; fresh has none → backfilled. - let future = (chrono::Utc::now() + chrono::Duration::hours(2)).to_rfc3339(); - let cached = codex_snapshot(win(50.0, Some(&future))); - let mut fresh = codex_snapshot(win(30.0, None)); - reset_backfill::codex_reset_backfill(&mut fresh, Some(&cached)); - assert_eq!(fresh.primary.resets_at.as_deref(), Some(future.as_str())); - // used_percent is NOT overwritten. - assert!((fresh.primary.used_percent - 30.0).abs() < f64::EPSILON); - } + #[test] - fn f6_does_not_backfill_stale_cached_reset() { - // Cached reset is in the past → not backfilled. - let past = (chrono::Utc::now() - chrono::Duration::hours(2)).to_rfc3339(); - let cached = codex_snapshot(win(50.0, Some(&past))); - let mut fresh = codex_snapshot(win(30.0, None)); - reset_backfill::codex_reset_backfill(&mut fresh, Some(&cached)); - assert!( - fresh.primary.resets_at.is_none(), - "stale reset not backfilled" + fn provider_timeout_error_message_is_plain_timeout() { + // The elapsed-timeout path reuses the provider-error path, which + // redacts the message; the user must still see "Timeout". + assert_eq!( + codexbar::logging::safe_error_message(codexbar::core::ProviderError::Timeout), + "Timeout" ); } - #[test] - fn f6_does_not_overwrite_existing_resets_at() { - // Fresh already has resets_at → cached not applied. - let future1 = (chrono::Utc::now() + chrono::Duration::hours(3)).to_rfc3339(); - let future2 = (chrono::Utc::now() + chrono::Duration::hours(5)).to_rfc3339(); - let cached = codex_snapshot(win(50.0, Some(&future2))); - let mut fresh = codex_snapshot(win(30.0, Some(&future1))); - reset_backfill::codex_reset_backfill(&mut fresh, Some(&cached)); - assert_eq!(fresh.primary.resets_at.as_deref(), Some(future1.as_str())); - } - #[test] - fn f6_skips_non_codex_provider() { - let future = (chrono::Utc::now() + chrono::Duration::hours(2)).to_rfc3339(); - let mut cached = codex_snapshot(win(50.0, Some(&future))); - cached.provider_id = "claude".into(); - let mut fresh = codex_snapshot(win(30.0, None)); - fresh.provider_id = "claude".into(); - reset_backfill::codex_reset_backfill(&mut fresh, Some(&cached)); - assert!(fresh.primary.resets_at.is_none(), "non-codex skip"); - } - #[test] - fn zai_five_hour_backfill_rejects_impossible_cached_reset() { - for (offset, should_backfill) in [ - (chrono::Duration::hours(1), true), - (chrono::Duration::hours(10), false), - ] { - let future = (chrono::Utc::now() + offset).to_rfc3339(); - let mut cached = codex_snapshot(win(50.0, Some(&future))); - cached.provider_id = "zai".into(); - let mut fresh = codex_snapshot(win(30.0, None)); - fresh.provider_id = "zai".into(); - fresh.primary.reset_description = Some("5-hour".into()); - reset_backfill::codex_reset_backfill(&mut fresh, Some(&cached)); - assert_eq!(fresh.primary.resets_at.is_some(), should_backfill); - assert!((fresh.primary.used_percent - 30.0).abs() < f64::EPSILON); - } - } - #[test] - fn f6_skips_when_no_cached_snapshot() { - let mut fresh = codex_snapshot(win(30.0, None)); - reset_backfill::codex_reset_backfill(&mut fresh, None); - assert!(fresh.primary.resets_at.is_none()); - } } diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers/reset_backfill.rs b/apps/desktop-tauri/src-tauri/src/commands/providers/reset_backfill.rs index c0aa66729a5..22046f038e9 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers/reset_backfill.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers/reset_backfill.rs @@ -70,7 +70,7 @@ fn backfill_slot_window( return; }; // A stale reset is worse than a missing one. - let Ok(cached_dt) = chrono::DateTime::parse_from_rfc3339(cached_reset) else { + let Some(cached_dt) = crate::commands::parse_utc(cached_reset) else { return; }; let now = chrono::Utc::now(); diff --git a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs index f7d6247d2ca..6c110d7dde1 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_presentation_tests.rs @@ -1,7 +1,11 @@ use super::*; use codexbar::core::ProviderId; -pub(crate) fn fake_snapshot(id: &str, display_name: &str, used_percent: f64) -> ProviderUsageSnapshot { +pub(crate) fn fake_snapshot( + id: &str, + display_name: &str, + used_percent: f64, +) -> ProviderUsageSnapshot { fake_snapshot_with(id, display_name, used_percent, None, None, None) } From 47c83c95b6151ec9a1869b6a43d2e68cd7119651 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:57:30 +0700 Subject: [PATCH 04/22] Collapse unreachable fetch-context arms and share browser cookie lookup --- .../src-tauri/src/commands/providers.rs | 195 ++++-------------- 1 file changed, 39 insertions(+), 156 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers.rs b/apps/desktop-tauri/src-tauri/src/commands/providers.rs index bcac222bb30..9906e5048d7 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers.rs @@ -102,6 +102,15 @@ pub(crate) fn build_fetch_context( let cookies_only_enrich_usage = provider.cookies_only_enrich_usage(); let browser_cookie_import = cookies_only_enrich_usage && matches!(cookie_source, "auto" | "browser" | "web"); + // #433: an explicitly selected, non-empty Claude manual cookie is + // authoritative. Do not let an active OAuth token account silently + // replace it; this keeps tray refresh behavior aligned with diagnose, + // whose Claude Auto path tries the supplied Web cookie before OAuth. + let manual_cookie_wins = cookie_source == "manual" + && provider.manual_cookie_precedes_token_account() + && stored_cookie + .as_deref() + .is_some_and(|cookie| !cookie.trim().is_empty()); let (mut source_mode, mut cookie_header, fails_closed_without_cookie) = if id .cookie_domain() .is_none() @@ -109,138 +118,26 @@ pub(crate) fn build_fetch_context( // The #725 wallet rule (#725 keeps Auto for wallet providers) folds into // the account-source decision; other no-cookie providers still honor // cookie_source below (the account-source pack's fix for the #619 merge). - let keeps_auto = - usage_source == SourceMode::Auto && provider.token_account_preserves_auto_source(); // A wallet provider keeps its saved usage source even with a selected // token account: rewriting it to OAuth would skip the wallet read // (#725 / upstream base-source resolver). - if keeps_auto || active_token_env.is_none() { - // A wallet provider keeps its saved usage source even with a - // selected token account: rewriting it to OAuth would skip the - // wallet read (#725 / upstream base-source resolver). - if active_token_env.is_some() { - (usage_source, None, false) - } else { - match cookie_source { - // #433: an explicitly selected, non-empty Claude manual cookie is - // authoritative. Do not let an active OAuth token account silently - // replace it; this keeps tray refresh behavior aligned with diagnose, - // whose Claude Auto path tries the supplied Web cookie before OAuth. - "manual" - if provider.manual_cookie_precedes_token_account() - && stored_cookie - .as_deref() - .is_some_and(|cookie| !cookie.trim().is_empty()) => - { - (SourceMode::Web, stored_cookie.clone(), false) - } - "auto" | "browser" | "web" => (usage_source, None, false), - _ => (usage_source, None, false), - } - } + let keeps_auto = + usage_source == SourceMode::Auto && provider.token_account_preserves_auto_source(); + if active_token_env.is_some() && keeps_auto { + (usage_source, None, false) + } else if active_token_env.is_some() + && provider.web_is_opt_in() + && usage_source != SourceMode::Web + { + // Opt-in web providers keep their default credential lane + // unless the usage source is explicitly Web. + (usage_source, None, false) + } else if manual_cookie_wins { + (SourceMode::Web, stored_cookie.clone(), false) + } else if active_token_env.is_some() { + (SourceMode::OAuth, None, false) } else { - match cookie_source { - // Opt-in web providers keep their default credential lane - // unless the usage source is explicitly Web; a stored or - // browser cookie must not turn Auto into Web. - _ if provider.web_is_opt_in() && usage_source != SourceMode::Web => { - (usage_source, None, false) - } - // #433: an explicitly selected, non-empty Claude manual cookie is - // authoritative. Do not let an active OAuth token account silently - // replace it; this keeps tray refresh behavior aligned with diagnose, - // whose Claude Auto path tries the supplied Web cookie before OAuth. - "manual" - if provider.manual_cookie_precedes_token_account() - && stored_cookie - .as_deref() - .is_some_and(|cookie| !cookie.trim().is_empty()) => - { - (SourceMode::Web, stored_cookie.clone(), false) - } - _ if active_token_env.is_some() => (SourceMode::OAuth, None, false), - // Charm Hyper: the cookie source only picks the session, and - // the usage source keeps routing. Off and an empty Manual - // source never import a browser session, while Auto keeps its - // API-key fallback. - "off" | "manual" if provider.cookie_source_scopes_session_only() => { - let cookie_header = if cookie_source == "manual" { - active_token_cookie.clone().or(stored_cookie) - } else { - None - }; - let source_mode = if provider.available_sources().contains(&usage_source) { - usage_source - } else { - SourceMode::Auto - }; - let cookie_missing = cookie_header.is_none(); - (source_mode, cookie_header, cookie_missing) - } - "off" if provider_uses_oauth_without_cookies(id, usage_source) => { - (SourceMode::OAuth, None, false) - } - "off" - if (has_kimi_code_api_key || has_opencodego_api_key) - && usage_source == SourceMode::Auto => - { - (SourceMode::Auto, None, false) - } - // Droid/Factory: cookie-off must never scrape browser cookies. Map to - // Cli (API-only in the provider) so Auto does not fall through to web. - "off" if id == ProviderId::Factory => (SourceMode::Cli, None, false), - "off" => (SourceMode::Cli, None, false), - "manual" => { - let cookie_header = active_token_cookie.clone().or(stored_cookie); - let fails_closed_without_cookie = cookie_header - .as_deref() - .is_none_or(|header| header.trim().is_empty()) - && provider.manual_empty_cookie_policy() - == ManualEmptyCookiePolicy::FailClosedWeb; - let source_mode = if (has_kimi_code_api_key || has_opencodego_api_key) - && usage_source == SourceMode::Auto - { - SourceMode::Auto - } else if let Some(mode) = grok_source_mode_for_manual_cookie(id, usage_source) - { - // Grok Switch writes ~/.grok/auth.json. Leftover grok.com - // cookies must not force Web, or Weekly/notifications keep - // showing the previous browser account. - mode - } else if cookie_header.is_some() { - SourceMode::Web - } else if fails_closed_without_cookie { - // The provider owns this policy; Web with no header means - // it fails closed instead of importing a browser account - // the user did not select. - SourceMode::Web - } else if provider_uses_oauth_without_cookies(id, usage_source) { - SourceMode::OAuth - } else { - SourceMode::Cli - }; - (source_mode, cookie_header, fails_closed_without_cookie) - } - // `browser` is accepted as a legacy alias from older settings. - "auto" | "browser" | "web" => { - // Claude resolves its cached cookie and browser fallback inside - // the provider; other providers retain the shell fallback. - let cookie_header = - active_token_cookie.clone().or(stored_cookie).or_else(|| { - if defer_provider_browser_cookie_lookup { - None - } else { - provider_cookie_domain(id, settings).and_then(|domain| { - codexbar::browser::cookies::get_cookie_header(domain) - .ok() - .filter(|h| !h.is_empty()) - }) - } - }); - (usage_source, cookie_header, false) - } - _ => (usage_source, stored_cookie, false), - } + (usage_source, None, false) } } else if cookies_only_enrich_usage { let cookie_header = (cookie_source == "manual") @@ -249,18 +146,7 @@ pub(crate) fn build_fetch_context( (usage_source, cookie_header, false) } else { match cookie_source { - // #433: an explicitly selected, non-empty Claude manual cookie is - // authoritative. Do not let an active OAuth token account silently - // replace it; this keeps tray refresh behavior aligned with diagnose, - // whose Claude Auto path tries the supplied Web cookie before OAuth. - "manual" - if provider.manual_cookie_precedes_token_account() - && stored_cookie - .as_deref() - .is_some_and(|cookie| !cookie.trim().is_empty()) => - { - (SourceMode::Web, stored_cookie.clone(), false) - } + _ if manual_cookie_wins => (SourceMode::Web, stored_cookie.clone(), false), _ if active_token_env.is_some() => (SourceMode::OAuth, None, false), // Opt-in web providers keep their default credential lane // unless the usage source is explicitly Web; a stored or @@ -295,9 +181,8 @@ pub(crate) fn build_fetch_context( { (SourceMode::Auto, None, false) } - // Droid/Factory: cookie-off must never scrape browser cookies. Map to + // Cookie-off must never scrape browser cookies (Droid/Factory). Map to // Cli (API-only in the provider) so Auto does not fall through to web. - "off" if id == ProviderId::Factory => (SourceMode::Cli, None, false), "off" => (SourceMode::Cli, None, false), "manual" => { let cookie_header = active_token_cookie.clone().or(stored_cookie); @@ -334,15 +219,9 @@ pub(crate) fn build_fetch_context( // Claude resolves its cached cookie and browser fallback inside // the provider; other providers retain the shell fallback. let cookie_header = active_token_cookie.clone().or(stored_cookie).or_else(|| { - if defer_provider_browser_cookie_lookup { - None - } else { - provider_cookie_domain(id, settings).and_then(|domain| { - codexbar::browser::cookies::get_cookie_header(domain) - .ok() - .filter(|h| !h.is_empty()) - }) - } + (!defer_provider_browser_cookie_lookup) + .then(|| browser_cookie_header(id, settings)) + .flatten() }); (usage_source, cookie_header, false) } @@ -368,11 +247,7 @@ pub(crate) fn build_fetch_context( .map(str::trim) .is_none_or(|s| s.is_empty()) { - cookie_header = provider_cookie_domain(id, settings).and_then(|domain| { - codexbar::browser::cookies::get_cookie_header(domain) - .ok() - .filter(|h| !h.is_empty()) - }); + cookie_header = browser_cookie_header(id, settings); } source_mode = SourceMode::Web; } @@ -455,6 +330,14 @@ pub(crate) fn build_fetch_context( } } +fn browser_cookie_header(id: ProviderId, settings: &Settings) -> Option { + provider_cookie_domain(id, settings).and_then(|domain| { + codexbar::browser::cookies::get_cookie_header(domain) + .ok() + .filter(|h| !h.is_empty()) + }) +} + fn provider_uses_oauth_without_cookies(id: ProviderId, usage_source: SourceMode) -> bool { match id { ProviderId::Claude => usage_source != SourceMode::Cli, From 079253691d7bf755d0dd1f4d277f0bf9fca3ecb1 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:02:35 +0700 Subject: [PATCH 05/22] Table-drive the build_fetch_context tests with one shared input helper --- .../src/commands/fetch_context_tests.rs | 813 +++++++++++++++ .../src-tauri/src/commands/mod.rs | 2 + .../src-tauri/src/commands/tests.rs | 972 +----------------- .../commands/token_account_source_tests.rs | 54 +- 4 files changed, 839 insertions(+), 1002 deletions(-) create mode 100644 apps/desktop-tauri/src-tauri/src/commands/fetch_context_tests.rs diff --git a/apps/desktop-tauri/src-tauri/src/commands/fetch_context_tests.rs b/apps/desktop-tauri/src-tauri/src/commands/fetch_context_tests.rs new file mode 100644 index 00000000000..d7b6d93bf0d --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/fetch_context_tests.rs @@ -0,0 +1,813 @@ +//! `build_fetch_context` resolution: one table for the plain input/output +//! cases, named tests for the cases with bespoke asserts. + +use std::collections::HashMap; + +use codexbar::core::{ + FetchContext, ProviderAccountData, ProviderId, SourceMode, TokenAccount, TokenAccountKind, + instantiate_provider, +}; +use codexbar::settings::{ApiKeys, ManualCookies, Settings}; + +/// Settings, stored secrets and token accounts for one provider. Unset fields +/// keep the defaults a fresh install has. +#[derive(Clone, Copy, Default)] +pub(super) struct CtxInput<'a> { + pub cookie_source: Option<&'a str>, + pub usage_source: Option<&'a str>, + pub region: Option<&'a str>, + pub manual_cookie: Option<&'a str>, + pub api_key: Option<&'a str>, + /// `(label, token)` pairs added in order. + pub accounts: &'a [(&'a str, &'a str)], + pub active: Option, +} + +pub(super) fn fetch_ctx(id: ProviderId, input: CtxInput<'_>) -> FetchContext { + let mut settings = Settings::default(); + if let Some(source) = input.cookie_source { + settings.set_cookie_source(id, source); + } + if let Some(source) = input.usage_source { + settings.set_usage_source(id, source); + } + if let Some(region) = input.region { + settings.set_api_region(id, region); + } + let mut cookies = ManualCookies::default(); + if let Some(header) = input.manual_cookie { + cookies.set(id.cli_name(), header); + } + let mut api_keys = ApiKeys::default(); + if let Some(key) = input.api_key { + api_keys.set(id.cli_name(), key, None); + } + let mut token_accounts = HashMap::new(); + if !input.accounts.is_empty() { + let mut data = ProviderAccountData::new(); + for &(label, token) in input.accounts { + data.add_account(TokenAccount::new(label, token)); + } + if let Some(index) = input.active { + data.set_active(index); + } + token_accounts.insert(id, data); + } + super::build_fetch_context(id, &settings, &cookies, &api_keys, &token_accounts) +} + +/// Expected context fields; `None` leaves a field unchecked. +#[derive(Default)] +struct Expect<'a> { + source: Option, + cookie: Option>, + api_key: Option>, + missing: Option, + region: Option>, + browser_import: Option, +} + +#[test] +fn fetch_context_resolves_each_case() { + use SourceMode::{Auto, Cli, OAuth, Web}; + let cases: &[(&str, ProviderId, CtxInput, Expect)] = &[ + ( + // Cursor does not support Cli; empty manual cookie remaps to Web (browser attempt). + "fetch_context_defaults_to_manual_cookies_without_browser_import", + ProviderId::Cursor, + CtxInput::default(), + Expect { + source: Some(Web), + ..Default::default() + }, + ), + ( + // Explicit cookie-off keeps Cli (no browser scrape). + "fetch_context_cursor_cookie_off_stays_cli", + ProviderId::Cursor, + CtxInput { + cookie_source: Some("off"), + ..Default::default() + }, + Expect { + source: Some(Cli), + cookie: Some(None), + ..Default::default() + }, + ), + ( + "fetch_context_grok_cookie_off_preserves_auto_oauth", + ProviderId::Grok, + CtxInput { + cookie_source: Some("off"), + usage_source: Some("auto"), + ..Default::default() + }, + Expect { + source: Some(OAuth), + cookie: Some(None), + ..Default::default() + }, + ), + ( + "fetch_context_grok_cookie_off_preserves_explicit_oauth", + ProviderId::Grok, + CtxInput { + cookie_source: Some("off"), + usage_source: Some("oauth"), + ..Default::default() + }, + Expect { + source: Some(OAuth), + cookie: Some(None), + ..Default::default() + }, + ), + ( + "fetch_context_grok_cookie_off_keeps_explicit_cli", + ProviderId::Grok, + CtxInput { + cookie_source: Some("off"), + usage_source: Some("cli"), + ..Default::default() + }, + Expect { + source: Some(Cli), + cookie: Some(None), + ..Default::default() + }, + ), + ( + "fetch_context_grok_empty_manual_preserves_auto_without_browser_import", + ProviderId::Grok, + CtxInput { + cookie_source: Some("manual"), + usage_source: Some("auto"), + ..Default::default() + }, + Expect { + source: Some(Auto), + cookie: Some(None), + ..Default::default() + }, + ), + ( + "fetch_context_grok_manual_cookie_keeps_auto_for_switched_login", + ProviderId::Grok, + CtxInput { + cookie_source: Some("manual"), + usage_source: Some("auto"), + manual_cookie: Some("sso=other-account"), + ..Default::default() + }, + Expect { + source: Some(Auto), + cookie: Some(Some("sso=other-account")), + ..Default::default() + }, + ), + ( + "fetch_context_grok_explicit_web_still_uses_manual_cookie", + ProviderId::Grok, + CtxInput { + cookie_source: Some("manual"), + usage_source: Some("web"), + manual_cookie: Some("sso=browser-account"), + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(Some("sso=browser-account")), + ..Default::default() + }, + ), + ( + // Zed browser billing is opt-in: neither the default manual cookie source + // nor a stored cookie may turn Auto into Web. + "fetch_context_zed_default_and_stored_cookie_keep_editor_credential_lane (default)", + ProviderId::Zed, + CtxInput::default(), + Expect { + source: Some(Auto), + cookie: Some(None), + missing: Some(false), + ..Default::default() + }, + ), + ( + "fetch_context_zed_default_and_stored_cookie_keep_editor_credential_lane (stored cookie)", + ProviderId::Zed, + CtxInput { + manual_cookie: Some("zed.session=stored"), + ..Default::default() + }, + Expect { + source: Some(Auto), + cookie: Some(None), + ..Default::default() + }, + ), + ( + "fetch_context_zed_explicit_web_uses_stored_cookie", + ProviderId::Zed, + CtxInput { + usage_source: Some("web"), + manual_cookie: Some("zed.session=stored"), + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(Some("zed.session=stored")), + ..Default::default() + }, + ), + ( + "fetch_context_opencode_empty_manual_remaps_to_web", + ProviderId::OpenCode, + CtxInput::default(), + Expect { + source: Some(Web), + ..Default::default() + }, + ), + ( + "fetch_context_replicate_empty_manual_fails_closed_without_browser_import", + ProviderId::Replicate, + CtxInput::default(), + Expect { + source: Some(Web), + cookie: Some(None), + missing: Some(true), + ..Default::default() + }, + ), + ( + // Manual with no stored header fails closed instead of using a browser. + "fetch_context_raycast_cookie_sources_never_import_in_the_shell (empty manual)", + ProviderId::Raycast, + CtxInput { + cookie_source: Some("manual"), + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(None), + missing: Some(true), + ..Default::default() + }, + ), + ( + // Off maps to the source the provider refuses, and never carries a header. + "fetch_context_raycast_cookie_sources_never_import_in_the_shell (off)", + ProviderId::Raycast, + CtxInput { + cookie_source: Some("off"), + manual_cookie: Some("__raycast_session=stored"), + ..Default::default() + }, + Expect { + source: Some(Cli), + cookie: Some(None), + ..Default::default() + }, + ), + ( + // Auto leaves browser resolution to the provider (Chrome only). + "fetch_context_raycast_cookie_sources_never_import_in_the_shell (auto)", + ProviderId::Raycast, + CtxInput { + cookie_source: Some("auto"), + ..Default::default() + }, + Expect { + source: Some(Auto), + cookie: Some(None), + missing: Some(false), + ..Default::default() + }, + ), + ( + "fetch_context_raycast_cookie_sources_never_import_in_the_shell (manual)", + ProviderId::Raycast, + CtxInput { + cookie_source: Some("manual"), + manual_cookie: Some("__raycast_session=stored"), + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(Some("__raycast_session=stored")), + ..Default::default() + }, + ), + ( + "fetch_context_ollama_empty_manual_fails_closed_without_browser_import", + ProviderId::Ollama, + CtxInput::default(), + Expect { + source: Some(Web), + cookie: Some(None), + missing: Some(true), + ..Default::default() + }, + ), + ( + "fetch_context_ollama_blank_manual_header_counts_as_missing", + ProviderId::Ollama, + CtxInput { + manual_cookie: Some(" "), + ..Default::default() + }, + Expect { + missing: Some(true), + ..Default::default() + }, + ), + ( + "fetch_context_ollama_pasted_header_is_not_reported_missing", + ProviderId::Ollama, + CtxInput { + manual_cookie: Some("__Secure-session=abc"), + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(Some("__Secure-session=abc")), + missing: Some(false), + ..Default::default() + }, + ), + ( + "fetch_context_ollama_auto_source_never_reports_manual_cookie_missing", + ProviderId::Ollama, + CtxInput { + cookie_source: Some("auto"), + ..Default::default() + }, + Expect { + missing: Some(false), + ..Default::default() + }, + ), + ( + "fetch_context_codex_manual_cookie_keeps_explicit_supported_source", + ProviderId::Codex, + CtxInput { + usage_source: Some("oauth"), + manual_cookie: Some("oai-did=abc"), + ..Default::default() + }, + Expect { + source: Some(OAuth), + ..Default::default() + }, + ), + ( + "fetch_context_claude_uses_oauth_without_manual_cookie", + ProviderId::Claude, + CtxInput::default(), + Expect { + source: Some(OAuth), + cookie: Some(None), + ..Default::default() + }, + ), + ( + "fetch_context_claude_web_source_defers_cookie_resolution_to_provider", + ProviderId::Claude, + CtxInput { + cookie_source: Some("browser"), + usage_source: Some("web"), + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(None), + ..Default::default() + }, + ), + ( + "fetch_context_claude_explicit_cli_source_still_uses_cli", + ProviderId::Claude, + CtxInput { + usage_source: Some("cli"), + ..Default::default() + }, + Expect { + source: Some(Cli), + cookie: Some(None), + ..Default::default() + }, + ), + ( + "fetch_context_manual_cookie_uses_web_without_browser_import", + ProviderId::Cursor, + CtxInput { + manual_cookie: Some("session=abc123"), + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(Some("session=abc123")), + ..Default::default() + }, + ), + ( + "fetch_context_api_key_provider_uses_auto_without_cookie_import", + ProviderId::DeepSeek, + CtxInput { + api_key: Some("sk-test"), + ..Default::default() + }, + Expect { + source: Some(Auto), + cookie: Some(None), + api_key: Some(Some("sk-test")), + ..Default::default() + }, + ), + ( + "fetch_context_kimi_api_key_preserves_auto_for_web_fallback", + ProviderId::Kimi, + CtxInput { + api_key: Some("sk-kimi-test"), + ..Default::default() + }, + Expect { + source: Some(Auto), + cookie: Some(None), + api_key: Some(Some("sk-kimi-test")), + ..Default::default() + }, + ), + ( + "fetch_context_opencodego_api_key_preserves_auto_for_api_overlay", + ProviderId::OpenCodeGo, + CtxInput { + api_key: Some("go-test"), + ..Default::default() + }, + Expect { + source: Some(Auto), + cookie: Some(None), + api_key: Some(Some("go-test")), + ..Default::default() + }, + ), + ( + "fetch_context_includes_minimax_region", + ProviderId::MiniMax, + CtxInput { + region: Some("cn"), + ..Default::default() + }, + Expect { + region: Some(Some("cn")), + ..Default::default() + }, + ), + ( + "fetch_context_token_account_uses_web_cookie_header", + ProviderId::Ollama, + CtxInput { + accounts: &[("Work", "abc123")], + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(Some("__Secure-session=abc123")), + ..Default::default() + }, + ), + ( + "fetch_context_claude_manual_cookie_beats_active_oauth_token_account", + ProviderId::Claude, + CtxInput { + cookie_source: Some("manual"), + usage_source: Some("auto"), + manual_cookie: Some("sessionKey=manual-session"), + accounts: &[("Claude OAuth", "[REDACTED_SECRET]")], + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(Some("sessionKey=manual-session")), + ..Default::default() + }, + ), + ( + "fetch_context_claude_oauth_token_account_uses_oauth", + ProviderId::Claude, + CtxInput { + accounts: &[("Claude OAuth", "sk-ant-oat01-abc123")], + ..Default::default() + }, + Expect { + source: Some(OAuth), + cookie: Some(None), + api_key: Some(Some("sk-ant-oat01-abc123")), + ..Default::default() + }, + ), + ( + "fetch_context_copilot_token_account_uses_oauth_api_key", + ProviderId::Copilot, + CtxInput { + accounts: &[("GitHub", "gho_testtoken")], + ..Default::default() + }, + Expect { + source: Some(OAuth), + cookie: Some(None), + api_key: Some(Some("gho_testtoken")), + ..Default::default() + }, + ), + ( + "fetch_context_claude_session_token_account_uses_web_cookie", + ProviderId::Claude, + CtxInput { + accounts: &[("Claude Web", "sessionKey=abc123")], + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(Some("sessionKey=abc123")), + api_key: Some(None), + ..Default::default() + }, + ), + ( + "fetch_context_token_account_takes_precedence_over_manual_cookie", + ProviderId::Cursor, + CtxInput { + manual_cookie: Some("manual=old"), + accounts: &[("Work", "WorkosCursorSessionToken=new")], + ..Default::default() + }, + Expect { + source: Some(Web), + cookie: Some(Some("WorkosCursorSessionToken=new")), + ..Default::default() + }, + ), + ( + "fetch_context_openrouter_token_account_overrides_stored_api_key", + ProviderId::OpenRouter, + CtxInput { + api_key: Some("sk-or-v1-stored-decoy"), + accounts: &[("Personal", "sk-or-v1-personal"), ("Work", "sk-or-v1-work")], + active: Some(1), + ..Default::default() + }, + Expect { + source: Some(OAuth), + cookie: Some(None), + api_key: Some(Some("sk-or-v1-work")), + ..Default::default() + }, + ), + ( + "fetch_context_openrouter_falls_back_to_stored_api_key_without_token_accounts", + ProviderId::OpenRouter, + CtxInput { + api_key: Some("sk-or-v1-stored"), + ..Default::default() + }, + Expect { + api_key: Some(Some("sk-or-v1-stored")), + ..Default::default() + }, + ), + ( + "muse_default_cookie_source_reads_no_browser_and_keeps_the_login_source", + ProviderId::Muse, + CtxInput::default(), + Expect { + source: Some(Auto), + cookie: Some(None), + browser_import: Some(false), + ..Default::default() + }, + ), + ( + "muse_cookie_source_off_never_reads_or_forwards_a_cookie", + ProviderId::Muse, + CtxInput { + cookie_source: Some("off"), + manual_cookie: Some("llama_dev_sess=abc"), + ..Default::default() + }, + Expect { + cookie: Some(None), + browser_import: Some(false), + ..Default::default() + }, + ), + ( + "muse_automatic_cookie_source_requests_the_browser_import", + ProviderId::Muse, + CtxInput { + cookie_source: Some("auto"), + manual_cookie: Some("llama_dev_sess=abc"), + ..Default::default() + }, + Expect { + source: Some(Auto), + cookie: Some(None), + browser_import: Some(true), + ..Default::default() + }, + ), + ( + "muse_manual_cookie_source_forwards_only_the_pasted_header", + ProviderId::Muse, + CtxInput { + cookie_source: Some("manual"), + manual_cookie: Some("llama_dev_sess=abc"), + ..Default::default() + }, + Expect { + cookie: Some(Some("llama_dev_sess=abc")), + browser_import: Some(false), + ..Default::default() + }, + ), + ( + "other_providers_never_request_the_muse_browser_import", + ProviderId::Cursor, + CtxInput::default(), + Expect { + browser_import: Some(false), + ..Default::default() + }, + ), + ]; + + for (name, id, input, expect) in cases { + let ctx = fetch_ctx(*id, *input); + if let Some(source) = expect.source { + assert_eq!(ctx.source_mode, source, "{name}: source_mode"); + } + if let Some(cookie) = expect.cookie { + assert_eq!( + ctx.manual_cookie_header.as_deref(), + cookie, + "{name}: manual_cookie_header" + ); + } + if let Some(api_key) = expect.api_key { + assert_eq!(ctx.api_key.as_deref(), api_key, "{name}: api_key"); + } + if let Some(missing) = expect.missing { + assert_eq!(ctx.manual_cookie_missing, missing, "{name}: missing"); + } + if let Some(region) = expect.region { + assert_eq!(ctx.api_region.as_deref(), region, "{name}: api_region"); + } + if let Some(browser_import) = expect.browser_import { + assert_eq!( + ctx.browser_cookie_import, browser_import, + "{name}: browser_cookie_import" + ); + } + } +} + +#[test] +fn fetch_context_carries_the_optional_details_opt_in_for_its_own_provider() { + let mut settings = Settings::default(); + settings.set_optional_details_enabled(ProviderId::LiteLLM, true); + let build = |id| { + super::build_fetch_context( + id, + &settings, + &ManualCookies::default(), + &ApiKeys::default(), + &HashMap::new(), + ) + }; + + assert!(build(ProviderId::LiteLLM).optional_details_enabled); + assert!(!build(ProviderId::Codex).optional_details_enabled); +} + +#[test] +fn fetch_context_codex_manual_cookie_never_forces_unsupported_web() { + // Default cookie source is "manual". Pasting a chatgpt.com cookie used to flip + // Codex into SourceMode::Web, which CodexProvider rejects with + // "Source mode 'Web' not supported for this provider" on every refresh. + let ctx = fetch_ctx( + ProviderId::Codex, + CtxInput { + manual_cookie: Some("oai-did=abc; __Secure-next-auth.session-token=xyz"), + ..Default::default() + }, + ); + + assert_eq!(ctx.source_mode, SourceMode::Auto); + assert!( + instantiate_provider(ProviderId::Codex) + .available_sources() + .contains(&ctx.source_mode) + ); +} + +#[test] +fn kimi_selected_account_forces_web_and_keeps_saved_region() { + let mut settings = Settings::default(); + settings.set_usage_source(ProviderId::Kimi, "oauth"); + settings.set_api_region(ProviderId::Kimi, "international"); + let mut accounts = HashMap::new(); + let mut data = ProviderAccountData::new(); + data.add_account(TokenAccount::new("Work", "selected-kimi-session")); + accounts.insert(ProviderId::Kimi, data); + + let ctx = super::build_fetch_context( + ProviderId::Kimi, + &settings, + &ManualCookies::default(), + &ApiKeys::default(), + &accounts, + ); + + assert_eq!(ctx.source_mode, SourceMode::Web); + assert_eq!( + ctx.manual_cookie_header.as_deref(), + Some("kimi-auth=selected-kimi-session") + ); + assert_eq!(ctx.api_key, None); + assert_eq!(ctx.api_region.as_deref(), Some("international")); + assert!(ctx.token_account_isolated); + assert_eq!(settings.usage_source(ProviderId::Kimi), "oauth"); + assert_eq!(settings.api_region(ProviderId::Kimi), "international"); +} + +#[test] +fn doubao_selected_account_forces_ark_api_and_ignores_saved_source() { + let ctx = fetch_ctx( + ProviderId::Doubao, + CtxInput { + usage_source: Some("cli"), + accounts: &[("Work", "selected-ark-key")], + ..Default::default() + }, + ); + + assert_eq!(ctx.source_mode, SourceMode::OAuth); + assert_eq!(ctx.api_key.as_deref(), Some("selected-ark-key")); + assert!(ctx.token_account_isolated); +} + +#[test] +fn opencodego_selected_api_account_overrides_global_key_without_changing_explicit_source() { + let build = |cookie_source, usage_source| { + fetch_ctx( + ProviderId::OpenCodeGo, + CtxInput { + cookie_source, + usage_source: Some(usage_source), + api_key: Some("global-key"), + accounts: &[("Work", "selected-account-key")], + ..Default::default() + }, + ) + }; + + let ctx = build(None, "auto"); + assert_eq!(ctx.source_mode, SourceMode::Auto); + assert_eq!(ctx.api_key.as_deref(), Some("selected-account-key")); + assert!(!ctx.auto_prefer_web); + assert!(ctx.token_account_isolated); + + for cookie_source in ["off", "manual"] { + let auto_ctx = build(Some(cookie_source), "auto"); + assert_eq!(auto_ctx.source_mode, SourceMode::Auto); + assert_eq!(auto_ctx.api_key.as_deref(), Some("selected-account-key")); + assert!(auto_ctx.manual_cookie_header.is_none()); + } + + for (saved_source, expected_source) in [("web", SourceMode::Web), ("cli", SourceMode::Cli)] { + let explicit_ctx = build(Some("off"), saved_source); + assert_eq!(explicit_ctx.source_mode, expected_source); + } +} + +#[test] +fn opencodego_selected_cookie_account_uses_web_route() { + let ctx = fetch_ctx( + ProviderId::OpenCodeGo, + CtxInput { + accounts: &[("Web", "Cookie: session=selected-session")], + ..Default::default() + }, + ); + + assert_eq!(ctx.source_mode, SourceMode::Web); + assert_eq!( + ctx.manual_cookie_header.as_deref(), + Some("Cookie: session=selected-session") + ); + assert_eq!(ctx.token_account_kind, Some(TokenAccountKind::Cookie)); + assert!(ctx.token_account_isolated); +} diff --git a/apps/desktop-tauri/src-tauri/src/commands/mod.rs b/apps/desktop-tauri/src-tauri/src/commands/mod.rs index 42590235748..451105d2003 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/mod.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/mod.rs @@ -81,6 +81,8 @@ pub use surface::*; pub use system::*; pub(crate) use usage_items::*; +#[cfg(test)] +mod fetch_context_tests; #[cfg(test)] mod last_good_owner_tests; #[cfg(test)] diff --git a/apps/desktop-tauri/src-tauri/src/commands/tests.rs b/apps/desktop-tauri/src-tauri/src/commands/tests.rs index 02e16821d50..93f1497466b 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/tests.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use super::{ NamedRateWindowSnapshot, ProviderSummary, ProviderUsageSnapshot, provider_cookie_source_lookup, provider_region_lookup, validate_external_url, validate_surface_target, @@ -8,11 +6,11 @@ use crate::state::AppState; use crate::surface::SurfaceMode; use crate::surface_target::SurfaceTarget; use codexbar::core::{ - FetchContext, ProviderAccountData, ProviderDisplayDetail, ProviderError, ProviderFetchResult, - ProviderId, ProviderInventoryItem, SourceMode, TokenAccount, instantiate_provider, + FetchContext, ProviderDisplayDetail, ProviderError, ProviderFetchResult, ProviderId, + ProviderInventoryItem, instantiate_provider, }; use codexbar::host::session::launch_block_reason; -use codexbar::settings::{ApiKeys, Language, ManualCookies, Settings}; +use codexbar::settings::{Language, Settings}; #[test] fn validate_surface_target_accepts_matching_target() { @@ -340,706 +338,6 @@ fn provider_cookie_source_set_rejects_unknown_provider() { assert!(err.contains("nope")); } -#[test] -fn fetch_context_defaults_to_manual_cookies_without_browser_import() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let api_keys = ApiKeys::default(); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::Cursor, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - // Cursor does not support Cli; empty manual cookie remaps to Web (browser attempt). - assert_eq!(ctx.source_mode, SourceMode::Web); -} - -#[test] -fn fetch_context_carries_the_optional_details_opt_in_for_its_own_provider() { - let mut settings = Settings::default(); - settings.set_optional_details_enabled(ProviderId::LiteLLM, true); - let build = |id| { - super::build_fetch_context( - id, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ) - }; - - assert!(build(ProviderId::LiteLLM).optional_details_enabled); - assert!(!build(ProviderId::Codex).optional_details_enabled); -} - -#[test] -fn fetch_context_cursor_cookie_off_stays_cli() { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Cursor, "off"); - let cookies = ManualCookies::default(); - let api_keys = ApiKeys::default(); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::Cursor, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - // Explicit cookie-off keeps Cli (no browser scrape). - assert_eq!(ctx.source_mode, SourceMode::Cli); - assert!(ctx.manual_cookie_header.is_none()); -} - -#[test] -fn fetch_context_grok_cookie_off_preserves_auto_oauth() { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Grok, "off"); - settings.set_usage_source(ProviderId::Grok, "auto"); - let ctx = super::build_fetch_context( - ProviderId::Grok, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::OAuth); - assert!(ctx.manual_cookie_header.is_none()); -} - -#[test] -fn fetch_context_grok_cookie_off_preserves_explicit_oauth() { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Grok, "off"); - settings.set_usage_source(ProviderId::Grok, "oauth"); - let ctx = super::build_fetch_context( - ProviderId::Grok, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::OAuth); - assert!(ctx.manual_cookie_header.is_none()); -} - -#[test] -fn fetch_context_grok_cookie_off_keeps_explicit_cli() { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Grok, "off"); - settings.set_usage_source(ProviderId::Grok, "cli"); - let ctx = super::build_fetch_context( - ProviderId::Grok, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::Cli); - assert!(ctx.manual_cookie_header.is_none()); -} - -#[test] -fn fetch_context_grok_empty_manual_preserves_auto_without_browser_import() { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Grok, "manual"); - settings.set_usage_source(ProviderId::Grok, "auto"); - let ctx = super::build_fetch_context( - ProviderId::Grok, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::Auto); - assert!(ctx.manual_cookie_header.is_none()); -} - -#[test] -fn fetch_context_grok_manual_cookie_keeps_auto_for_switched_login() { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Grok, "manual"); - settings.set_usage_source(ProviderId::Grok, "auto"); - let mut cookies = ManualCookies::default(); - cookies.set("grok", "sso=other-account"); - let ctx = super::build_fetch_context( - ProviderId::Grok, - &settings, - &cookies, - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::Auto); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("sso=other-account") - ); -} - -#[test] -fn fetch_context_grok_explicit_web_still_uses_manual_cookie() { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Grok, "manual"); - settings.set_usage_source(ProviderId::Grok, "web"); - let mut cookies = ManualCookies::default(); - cookies.set("grok", "sso=browser-account"); - let ctx = super::build_fetch_context( - ProviderId::Grok, - &settings, - &cookies, - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("sso=browser-account") - ); -} - -#[test] -fn fetch_context_zed_default_and_stored_cookie_keep_editor_credential_lane() { - // Zed browser billing is opt-in: neither the default manual cookie source - // nor a stored cookie may turn Auto into Web. - let ctx = super::build_fetch_context( - ProviderId::Zed, - &Settings::default(), - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ); - assert_eq!(ctx.source_mode, SourceMode::Auto); - assert!(ctx.manual_cookie_header.is_none()); - assert!(!ctx.manual_cookie_missing); - - let mut cookies = ManualCookies::default(); - cookies.set("zed", "zed.session=stored"); - let ctx = super::build_fetch_context( - ProviderId::Zed, - &Settings::default(), - &cookies, - &ApiKeys::default(), - &HashMap::new(), - ); - assert_eq!(ctx.source_mode, SourceMode::Auto); - assert!(ctx.manual_cookie_header.is_none()); -} - -#[test] -fn fetch_context_zed_explicit_web_uses_stored_cookie() { - let mut settings = Settings::default(); - settings.set_usage_source(ProviderId::Zed, "web"); - let mut cookies = ManualCookies::default(); - cookies.set("zed", "zed.session=stored"); - let ctx = super::build_fetch_context( - ProviderId::Zed, - &settings, - &cookies, - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("zed.session=stored") - ); -} - -#[test] -fn fetch_context_opencode_empty_manual_remaps_to_web() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let api_keys = ApiKeys::default(); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::OpenCode, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); -} - -#[test] -fn kimi_selected_account_forces_web_and_keeps_saved_region() { - let mut settings = Settings::default(); - settings.set_usage_source(ProviderId::Kimi, "oauth"); - settings.set_api_region(ProviderId::Kimi, "international"); - let mut accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Work", "selected-kimi-session")); - accounts.insert(ProviderId::Kimi, data); - - let ctx = super::build_fetch_context( - ProviderId::Kimi, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("kimi-auth=selected-kimi-session") - ); - assert_eq!(ctx.api_key, None); - assert_eq!(ctx.api_region.as_deref(), Some("international")); - assert!(ctx.token_account_isolated); - assert_eq!(settings.usage_source(ProviderId::Kimi), "oauth"); - assert_eq!(settings.api_region(ProviderId::Kimi), "international"); -} - -#[test] -fn doubao_selected_account_forces_ark_api_and_ignores_saved_source() { - let mut settings = Settings::default(); - settings.set_usage_source(ProviderId::Doubao, "cli"); - let mut accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Work", "selected-ark-key")); - accounts.insert(ProviderId::Doubao, data); - - let ctx = super::build_fetch_context( - ProviderId::Doubao, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::OAuth); - assert_eq!(ctx.api_key.as_deref(), Some("selected-ark-key")); - assert!(ctx.token_account_isolated); -} - -#[test] -fn opencodego_selected_api_account_overrides_global_key_without_changing_explicit_source() { - let mut settings = Settings::default(); - settings.set_usage_source(ProviderId::OpenCodeGo, "auto"); - let mut keys = ApiKeys::default(); - keys.set("opencodego", "global-key", None); - let mut accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Work", "selected-account-key")); - accounts.insert(ProviderId::OpenCodeGo, data); - - let ctx = super::build_fetch_context( - ProviderId::OpenCodeGo, - &settings, - &ManualCookies::default(), - &keys, - &accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Auto); - assert_eq!(ctx.api_key.as_deref(), Some("selected-account-key")); - assert!(!ctx.auto_prefer_web); - assert!(ctx.token_account_isolated); - - for cookie_source in ["off", "manual"] { - settings.set_cookie_source(ProviderId::OpenCodeGo, cookie_source); - settings.set_usage_source(ProviderId::OpenCodeGo, "auto"); - let auto_ctx = super::build_fetch_context( - ProviderId::OpenCodeGo, - &settings, - &ManualCookies::default(), - &keys, - &accounts, - ); - assert_eq!(auto_ctx.source_mode, SourceMode::Auto); - assert_eq!(auto_ctx.api_key.as_deref(), Some("selected-account-key")); - assert!(auto_ctx.manual_cookie_header.is_none()); - } - - for (saved_source, expected_source) in [("web", SourceMode::Web), ("cli", SourceMode::Cli)] { - settings.set_cookie_source(ProviderId::OpenCodeGo, "off"); - settings.set_usage_source(ProviderId::OpenCodeGo, saved_source); - let explicit_ctx = super::build_fetch_context( - ProviderId::OpenCodeGo, - &settings, - &ManualCookies::default(), - &keys, - &accounts, - ); - assert_eq!(explicit_ctx.source_mode, expected_source); - } -} - -#[test] -fn opencodego_selected_cookie_account_uses_web_route() { - let settings = Settings::default(); - let mut accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Web", "Cookie: session=selected-session")); - accounts.insert(ProviderId::OpenCodeGo, data); - - let ctx = super::build_fetch_context( - ProviderId::OpenCodeGo, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("Cookie: session=selected-session") - ); - assert_eq!( - ctx.token_account_kind, - Some(codexbar::core::TokenAccountKind::Cookie) - ); - assert!(ctx.token_account_isolated); -} - -#[test] -fn fetch_context_replicate_empty_manual_fails_closed_without_browser_import() { - let settings = Settings::default(); - let ctx = super::build_fetch_context( - ProviderId::Replicate, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert!(ctx.manual_cookie_header.is_none()); - assert!(ctx.manual_cookie_missing); -} - -#[test] -fn fetch_context_raycast_cookie_sources_never_import_in_the_shell() { - let build = |source: &str, stored: Option<&str>| { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Raycast, source); - let mut cookies = ManualCookies::default(); - if let Some(stored) = stored { - cookies.set(ProviderId::Raycast.cli_name(), stored); - } - super::build_fetch_context( - ProviderId::Raycast, - &settings, - &cookies, - &ApiKeys::default(), - &HashMap::new(), - ) - }; - - // Manual with no stored header fails closed instead of using a browser. - let empty_manual = build("manual", None); - assert_eq!(empty_manual.source_mode, SourceMode::Web); - assert!(empty_manual.manual_cookie_header.is_none()); - assert!(empty_manual.manual_cookie_missing); - - // Off maps to the source the provider refuses, and never carries a header. - let off = build("off", Some("__raycast_session=stored")); - assert_eq!(off.source_mode, SourceMode::Cli); - assert!(off.manual_cookie_header.is_none()); - - // Auto leaves browser resolution to the provider (Chrome only). - let auto = build("auto", None); - assert_eq!(auto.source_mode, SourceMode::Auto); - assert!(auto.manual_cookie_header.is_none()); - assert!(!auto.manual_cookie_missing); - - let manual = build("manual", Some("__raycast_session=stored")); - assert_eq!(manual.source_mode, SourceMode::Web); - assert_eq!( - manual.manual_cookie_header.as_deref(), - Some("__raycast_session=stored") - ); -} - -#[test] -fn fetch_context_ollama_empty_manual_fails_closed_without_browser_import() { - let settings = Settings::default(); - let ctx = super::build_fetch_context( - ProviderId::Ollama, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert!(ctx.manual_cookie_header.is_none()); - assert!(ctx.manual_cookie_missing); -} - -#[test] -fn fetch_context_ollama_blank_manual_header_counts_as_missing() { - let mut cookies = ManualCookies::default(); - cookies.set(ProviderId::Ollama.cli_name(), " "); - let ctx = super::build_fetch_context( - ProviderId::Ollama, - &Settings::default(), - &cookies, - &ApiKeys::default(), - &HashMap::new(), - ); - - assert!(ctx.manual_cookie_missing); -} - -#[test] -fn fetch_context_ollama_pasted_header_is_not_reported_missing() { - let mut cookies = ManualCookies::default(); - cookies.set(ProviderId::Ollama.cli_name(), "__Secure-session=abc"); - let ctx = super::build_fetch_context( - ProviderId::Ollama, - &Settings::default(), - &cookies, - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("__Secure-session=abc") - ); - assert!(!ctx.manual_cookie_missing); -} - -#[test] -fn fetch_context_ollama_auto_source_never_reports_manual_cookie_missing() { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Ollama, "auto"); - let ctx = super::build_fetch_context( - ProviderId::Ollama, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ); - - assert!(!ctx.manual_cookie_missing); -} - -#[test] -fn fetch_context_codex_manual_cookie_never_forces_unsupported_web() { - // Default cookie source is "manual". Pasting a chatgpt.com cookie used to flip - // Codex into SourceMode::Web, which CodexProvider rejects with - // "Source mode 'Web' not supported for this provider" on every refresh. - let settings = Settings::default(); - let mut cookies = ManualCookies::default(); - cookies.set( - ProviderId::Codex.cli_name(), - "oai-did=abc; __Secure-next-auth.session-token=xyz", - ); - - let ctx = super::build_fetch_context( - ProviderId::Codex, - &settings, - &cookies, - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::Auto); - assert!( - instantiate_provider(ProviderId::Codex) - .available_sources() - .contains(&ctx.source_mode) - ); -} - -#[test] -fn fetch_context_codex_manual_cookie_keeps_explicit_supported_source() { - let mut settings = Settings::default(); - settings.set_usage_source(ProviderId::Codex, "oauth"); - let mut cookies = ManualCookies::default(); - cookies.set(ProviderId::Codex.cli_name(), "oai-did=abc"); - - let ctx = super::build_fetch_context( - ProviderId::Codex, - &settings, - &cookies, - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::OAuth); -} - -#[test] -fn fetch_context_claude_uses_oauth_without_manual_cookie() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let api_keys = ApiKeys::default(); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::Claude, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::OAuth); - assert!(ctx.manual_cookie_header.is_none()); -} - -#[test] -fn fetch_context_claude_web_source_defers_cookie_resolution_to_provider() { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Claude, "browser"); - settings.set_usage_source(ProviderId::Claude, "web"); - - let ctx = super::build_fetch_context( - ProviderId::Claude, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert!(ctx.manual_cookie_header.is_none()); -} - -#[test] -fn fetch_context_claude_explicit_cli_source_still_uses_cli() { - let mut settings = Settings::default(); - settings.set_usage_source(ProviderId::Claude, "cli"); - let cookies = ManualCookies::default(); - let api_keys = ApiKeys::default(); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::Claude, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Cli); - assert!(ctx.manual_cookie_header.is_none()); -} - -#[test] -fn fetch_context_manual_cookie_uses_web_without_browser_import() { - let settings = Settings::default(); - let mut cookies = ManualCookies::default(); - cookies.set("cursor", "session=abc123"); - let api_keys = ApiKeys::default(); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::Cursor, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert_eq!(ctx.manual_cookie_header.as_deref(), Some("session=abc123")); -} - -#[test] -fn fetch_context_api_key_provider_uses_auto_without_cookie_import() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let mut api_keys = ApiKeys::default(); - api_keys.set("deepseek", "sk-test", None); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::DeepSeek, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Auto); - assert!(ctx.manual_cookie_header.is_none()); - assert_eq!(ctx.api_key.as_deref(), Some("sk-test")); -} - -#[test] -fn fetch_context_kimi_api_key_preserves_auto_for_web_fallback() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let mut api_keys = ApiKeys::default(); - api_keys.set("kimi", "sk-kimi-test", None); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::Kimi, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Auto); - assert!(ctx.manual_cookie_header.is_none()); - assert_eq!(ctx.api_key.as_deref(), Some("sk-kimi-test")); -} - -#[test] -fn fetch_context_opencodego_api_key_preserves_auto_for_api_overlay() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let mut api_keys = ApiKeys::default(); - api_keys.set("opencodego", "go-test", None); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::OpenCodeGo, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Auto); - assert!(ctx.manual_cookie_header.is_none()); - assert_eq!(ctx.api_key.as_deref(), Some("go-test")); -} - -#[test] -fn fetch_context_includes_minimax_region() { - let mut settings = Settings::default(); - settings.set_api_region(ProviderId::MiniMax, "cn"); - let cookies = ManualCookies::default(); - let api_keys = ApiKeys::default(); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::MiniMax, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.api_region.as_deref(), Some("cn")); -} - #[test] fn provider_dashboard_url_uses_selected_regional_console() { let mut settings = Settings::default(); @@ -1074,202 +372,6 @@ fn api_key_catalog_providers_have_metadata_dashboard_urls() { } } -#[test] -fn fetch_context_token_account_uses_web_cookie_header() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let api_keys = ApiKeys::default(); - let mut token_accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Work", "abc123")); - token_accounts.insert(ProviderId::Ollama, data); - - let ctx = super::build_fetch_context( - ProviderId::Ollama, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("__Secure-session=abc123") - ); -} - -#[test] -fn fetch_context_claude_manual_cookie_beats_active_oauth_token_account() { - let mut settings = Settings::default(); - settings.set_cookie_source(ProviderId::Claude, "manual"); - settings.set_usage_source(ProviderId::Claude, "auto"); - let mut cookies = ManualCookies::default(); - cookies.set("claude", "sessionKey=manual-session"); - let api_keys = ApiKeys::default(); - let mut token_accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Claude OAuth", "[REDACTED_SECRET]")); - token_accounts.insert(ProviderId::Claude, data); - - let ctx = super::build_fetch_context( - ProviderId::Claude, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("sessionKey=manual-session") - ); -} - -#[test] -fn fetch_context_claude_oauth_token_account_uses_oauth() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let api_keys = ApiKeys::default(); - let mut token_accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Claude OAuth", "sk-ant-oat01-abc123")); - token_accounts.insert(ProviderId::Claude, data); - - let ctx = super::build_fetch_context( - ProviderId::Claude, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::OAuth); - assert!(ctx.manual_cookie_header.is_none()); - assert_eq!(ctx.api_key.as_deref(), Some("sk-ant-oat01-abc123")); -} - -#[test] -fn fetch_context_copilot_token_account_uses_oauth_api_key() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let api_keys = ApiKeys::default(); - let mut token_accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("GitHub", "gho_testtoken")); - token_accounts.insert(ProviderId::Copilot, data); - - let ctx = super::build_fetch_context( - ProviderId::Copilot, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::OAuth); - assert!(ctx.manual_cookie_header.is_none()); - assert_eq!(ctx.api_key.as_deref(), Some("gho_testtoken")); -} - -#[test] -fn fetch_context_claude_session_token_account_uses_web_cookie() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let api_keys = ApiKeys::default(); - let mut token_accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Claude Web", "sessionKey=abc123")); - token_accounts.insert(ProviderId::Claude, data); - - let ctx = super::build_fetch_context( - ProviderId::Claude, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("sessionKey=abc123") - ); - assert!(ctx.api_key.is_none()); -} - -#[test] -fn fetch_context_token_account_takes_precedence_over_manual_cookie() { - let settings = Settings::default(); - let mut cookies = ManualCookies::default(); - cookies.set("cursor", "manual=old"); - let api_keys = ApiKeys::default(); - let mut token_accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Work", "WorkosCursorSessionToken=new")); - token_accounts.insert(ProviderId::Cursor, data); - - let ctx = super::build_fetch_context( - ProviderId::Cursor, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::Web); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("WorkosCursorSessionToken=new") - ); -} - -#[test] -fn fetch_context_openrouter_token_account_overrides_stored_api_key() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let mut api_keys = ApiKeys::default(); - api_keys.set("openrouter", "sk-or-v1-stored-decoy", None); - let mut token_accounts = HashMap::new(); - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Personal", "sk-or-v1-personal")); - data.add_account(TokenAccount::new("Work", "sk-or-v1-work")); - data.set_active(1); - token_accounts.insert(ProviderId::OpenRouter, data); - - let ctx = super::build_fetch_context( - ProviderId::OpenRouter, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.source_mode, SourceMode::OAuth); - assert!(ctx.manual_cookie_header.is_none()); - assert_eq!(ctx.api_key.as_deref(), Some("sk-or-v1-work")); -} - -#[test] -fn fetch_context_openrouter_falls_back_to_stored_api_key_without_token_accounts() { - let settings = Settings::default(); - let cookies = ManualCookies::default(); - let mut api_keys = ApiKeys::default(); - api_keys.set("openrouter", "sk-or-v1-stored", None); - let token_accounts = HashMap::new(); - - let ctx = super::build_fetch_context( - ProviderId::OpenRouter, - &settings, - &cookies, - &api_keys, - &token_accounts, - ); - - assert_eq!(ctx.api_key.as_deref(), Some("sk-or-v1-stored")); -} - #[test] fn provider_region_set_rejects_non_regional_provider() { let mut s = Settings::default(); @@ -2611,71 +1713,3 @@ fn bootstrap_catalog_depends_only_on_supplied_settings() { "a deprecated provider that is not enabled stays hidden" ); } - -fn muse_fetch_context(cookie_source: Option<&str>, stored_cookie: Option<&str>) -> FetchContext { - let mut settings = Settings::default(); - if let Some(source) = cookie_source { - settings.set_cookie_source(ProviderId::Muse, source); - } - let mut cookies = ManualCookies::default(); - if let Some(header) = stored_cookie { - cookies.set(ProviderId::Muse.cli_name(), header); - } - super::build_fetch_context( - ProviderId::Muse, - &settings, - &cookies, - &ApiKeys::default(), - &HashMap::new(), - ) -} - -#[test] -fn muse_default_cookie_source_reads_no_browser_and_keeps_the_login_source() { - let ctx = muse_fetch_context(None, None); - - assert!(!ctx.browser_cookie_import); - assert!(ctx.manual_cookie_header.is_none()); - assert_eq!(ctx.source_mode, SourceMode::Auto); -} - -#[test] -fn muse_cookie_source_off_never_reads_or_forwards_a_cookie() { - let ctx = muse_fetch_context(Some("off"), Some("llama_dev_sess=abc")); - - assert!(!ctx.browser_cookie_import); - assert!(ctx.manual_cookie_header.is_none()); -} - -#[test] -fn muse_automatic_cookie_source_requests_the_browser_import() { - let ctx = muse_fetch_context(Some("auto"), Some("llama_dev_sess=abc")); - - assert!(ctx.browser_cookie_import); - assert!(ctx.manual_cookie_header.is_none()); - assert_eq!(ctx.source_mode, SourceMode::Auto); -} - -#[test] -fn muse_manual_cookie_source_forwards_only_the_pasted_header() { - let ctx = muse_fetch_context(Some("manual"), Some("llama_dev_sess=abc")); - - assert!(!ctx.browser_cookie_import); - assert_eq!( - ctx.manual_cookie_header.as_deref(), - Some("llama_dev_sess=abc") - ); -} - -#[test] -fn other_providers_never_request_the_muse_browser_import() { - let ctx = super::build_fetch_context( - ProviderId::Cursor, - &Settings::default(), - &ManualCookies::default(), - &ApiKeys::default(), - &HashMap::new(), - ); - - assert!(!ctx.browser_cookie_import); -} diff --git a/apps/desktop-tauri/src-tauri/src/commands/token_account_source_tests.rs b/apps/desktop-tauri/src-tauri/src/commands/token_account_source_tests.rs index ee1cee114e7..099a3297e9e 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/token_account_source_tests.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/token_account_source_tests.rs @@ -1,38 +1,28 @@ //! Source-mode resolution for selected token accounts. -use std::collections::HashMap; - -use codexbar::core::{ - ProviderAccountData, ProviderId, SourceMode, TokenAccount, instantiate_provider, -}; -use codexbar::settings::{ApiKeys, ManualCookies, Settings}; - -fn token_accounts(id: ProviderId, token: &str) -> HashMap { - let mut data = ProviderAccountData::new(); - data.add_account(TokenAccount::new("Work", token)); - HashMap::from([(id, data)]) -} - -fn context_with_usage_source( - id: ProviderId, - usage_source: &str, - accounts: &HashMap, -) -> codexbar::core::FetchContext { - let mut settings = Settings::default(); - settings.set_usage_source(id, usage_source); - super::build_fetch_context( - id, - &settings, - &ManualCookies::default(), - &ApiKeys::default(), - accounts, +use codexbar::core::{FetchContext, ProviderId, SourceMode, instantiate_provider}; + +use super::fetch_context_tests::{CtxInput, fetch_ctx}; + +fn huggingface_context(usage_source: &str, with_account: bool) -> FetchContext { + let accounts: &[(&str, &str)] = if with_account { + &[("Work", "hf_account_token")] + } else { + &[] + }; + fetch_ctx( + ProviderId::HuggingFace, + CtxInput { + usage_source: Some(usage_source), + accounts, + ..Default::default() + }, ) } #[test] fn huggingface_token_account_keeps_auto_so_the_wallet_is_still_read() { - let accounts = token_accounts(ProviderId::HuggingFace, "hf_account_token"); - let ctx = context_with_usage_source(ProviderId::HuggingFace, "auto", &accounts); + let ctx = huggingface_context("auto", true); assert_eq!(ctx.source_mode, SourceMode::Auto); assert_eq!(ctx.api_key.as_deref(), Some("hf_account_token")); @@ -40,8 +30,7 @@ fn huggingface_token_account_keeps_auto_so_the_wallet_is_still_read() { #[test] fn huggingface_token_account_with_explicit_api_source_stays_api_only() { - let accounts = token_accounts(ProviderId::HuggingFace, "hf_account_token"); - let ctx = context_with_usage_source(ProviderId::HuggingFace, "oauth", &accounts); + let ctx = huggingface_context("oauth", true); assert_eq!(ctx.source_mode, SourceMode::OAuth); assert_eq!(ctx.api_key.as_deref(), Some("hf_account_token")); @@ -49,16 +38,15 @@ fn huggingface_token_account_with_explicit_api_source_stays_api_only() { #[test] fn huggingface_token_account_with_an_unsupported_stored_source_falls_back_to_api() { - let accounts = token_accounts(ProviderId::HuggingFace, "hf_account_token"); for stale in ["web", "cli"] { - let ctx = context_with_usage_source(ProviderId::HuggingFace, stale, &accounts); + let ctx = huggingface_context(stale, true); assert_eq!(ctx.source_mode, SourceMode::OAuth, "{stale}"); } } #[test] fn huggingface_without_a_token_account_follows_the_usage_source() { - let ctx = context_with_usage_source(ProviderId::HuggingFace, "auto", &HashMap::new()); + let ctx = huggingface_context("auto", false); assert_eq!(ctx.source_mode, SourceMode::Auto); } From 3ee11f95ff2ff65d72e720694fa5bc2557c7d675 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:04:02 +0700 Subject: [PATCH 06/22] Build test fetch results with ProviderFetchResult::new --- .../src-tauri/src/commands/tests.rs | 242 ++++-------------- 1 file changed, 53 insertions(+), 189 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/tests.rs b/apps/desktop-tauri/src-tauri/src/commands/tests.rs index 93f1497466b..55be4c2b78e 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/tests.rs @@ -441,20 +441,10 @@ fn provider_detail_carries_openai_daily_usage_only_when_present() { #[test] fn usage_item_descriptors_keep_raw_ids_and_redact_titles() { let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "OAuth".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), + "OAuth", + ); let mut snapshot = ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); snapshot.primary_label = Some("Account owner@example.com".to_string()); @@ -485,27 +475,16 @@ fn usage_item_descriptors_keep_raw_ids_and_redact_titles() { #[test] fn usage_item_descriptors_include_detail_sections() { let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), - cost: None, - wayfinder_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: vec![ - codexbar::core::ProviderDisplayDetail::new("d1", "Rate limits", "5") - .expect("valid detail"), - codexbar::core::ProviderDisplayDetail::new("d2", "Rate limits", "6") - .expect("valid detail"), - codexbar::core::ProviderDisplayDetail::new("d3", "owner@example.com quota", "1") - .expect("valid detail"), - ], - source_label: "OAuth".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - open_ai_api_usage: None, - last_good_owner: None, - }; + let mut result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), + "OAuth", + ); + result.display_details = vec![ + codexbar::core::ProviderDisplayDetail::new("d1", "Rate limits", "5").expect("valid detail"), + codexbar::core::ProviderDisplayDetail::new("d2", "Rate limits", "6").expect("valid detail"), + codexbar::core::ProviderDisplayDetail::new("d3", "owner@example.com quota", "1") + .expect("valid detail"), + ]; let snapshot = ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); @@ -746,20 +725,10 @@ fn provider_fetch_timeout_respects_context_web_timeout_with_cap() { #[test] fn provider_cache_upsert_replaces_existing_provider() { let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "CLI".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), + "CLI", + ); let mut first = ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); let mut second = first.clone(); @@ -777,20 +746,10 @@ fn provider_cache_upsert_replaces_existing_provider() { #[test] fn provider_cache_prunes_disabled_providers() { let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "CLI".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), + "CLI", + ); let codex = ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); let claude_meta = instantiate_provider(ProviderId::Claude).metadata().clone(); @@ -816,20 +775,10 @@ fn superseded_refresh_generation_is_not_current() { fn claude_transient_auth_failure_preserves_first_last_good_snapshot() { let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "OAuth".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), + "OAuth", + ); let good = ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); let snapshot = ProviderUsageSnapshot::from_error( @@ -856,20 +805,10 @@ fn claude_transient_auth_failure_preserves_first_last_good_snapshot() { #[test] fn codex_transient_transport_failure_helper_uses_typed_policy() { let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "OAuth".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), + "OAuth", + ); let good = ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); let snapshot = ProviderUsageSnapshot::from_error( @@ -895,20 +834,10 @@ fn codex_transient_transport_failure_helper_uses_typed_policy() { #[test] fn claude_repeated_auth_failure_surfaces_error() { let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "OAuth".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), + "OAuth", + ); let good = ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); let first_error = ProviderUsageSnapshot::from_error( @@ -941,20 +870,10 @@ fn claude_repeated_auth_failure_surfaces_error() { #[test] fn claude_cloudflare_challenge_retains_prior_usage_while_surfaceing_guidance() { let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "OAuth".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), + "OAuth", + ); let good = ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); let challenge = codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE; @@ -998,20 +917,10 @@ fn claude_cloudflare_challenge_retains_prior_usage_while_surfaceing_guidance() { #[test] fn claude_cloudflare_challenge_keeps_prior_usage_when_guidance_surfaces() { let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "Web".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), + "Web", + ); let mut good = ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); good.updated_at = "2026-09-01T00:00:00Z".to_string(); @@ -1052,20 +961,11 @@ fn claude_cloudflare_challenge_keeps_prior_usage_when_guidance_surfaces() { #[test] fn claude_cli_parse_failure_keeps_last_good_every_time() { let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(17.0)), - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "CLI".to_string(), - has_successful_claude_cli_quota: true, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let mut result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(17.0)), + "CLI", + ); + result.has_successful_claude_cli_quota = true; let good = ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); let err = ProviderUsageSnapshot::from_error( @@ -1102,20 +1002,10 @@ fn claude_cli_parse_failure_keeps_last_good_every_time() { #[test] fn claude_hard_credentials_missing_does_not_preserve_stale() { let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult { - usage: codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(17.0)), - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "OAuth".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(17.0)), + "OAuth", + ); let good = ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); let err = ProviderUsageSnapshot::from_error( @@ -1312,20 +1202,7 @@ fn japanese_provider_snapshot_localizes_weekly_label() { let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); let usage = codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)) .with_secondary(codexbar::core::RateWindow::new(20.0)); - let result = ProviderFetchResult { - usage, - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "OAuth".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new(usage, "OAuth"); let snapshot = ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); @@ -1349,20 +1226,7 @@ fn japanese_provider_snapshot_localizes_pace_reserve_description() { ); let usage = codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)) .with_secondary(secondary); - let result = ProviderFetchResult { - usage, - cost: None, - wayfinder_usage: None, - open_ai_api_usage: None, - inventory: Vec::new(), - reset_credits: None, - display_details: Vec::new(), - source_label: "OAuth".to_string(), - has_successful_claude_cli_quota: false, - pace_authoritative: true, - account_identity: None, - last_good_owner: None, - }; + let result = ProviderFetchResult::new(usage, "OAuth"); let snapshot = ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); From 72d5a10fe41ff1d65ea632da0cc698f3fdde343a Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:09:29 +0700 Subject: [PATCH 07/22] Delete the PopOut-only positioning chain --- .../src-tauri/src/shell/geometry.rs | 43 --- .../src-tauri/src/shell/position.rs | 96 +------ .../src-tauri/src/shell/tests.rs | 247 +----------------- .../src-tauri/src/window_positioner.rs | 172 ------------ 4 files changed, 4 insertions(+), 554 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/shell/geometry.rs b/apps/desktop-tauri/src-tauri/src/shell/geometry.rs index 516aaca0611..ed574f72eb7 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/geometry.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/geometry.rs @@ -81,19 +81,6 @@ pub(super) fn monitor_placement(monitor: &tauri::Monitor) -> MonitorPlacement { } } -pub(super) fn popout_position( - anchor_rect: Option<&Rect>, - monitor: &MonitorPlacement, - panel_size: &PanelSize, -) -> (i32, i32) { - window_positioner::calculate_popout_position( - anchor_rect, - &monitor.work_area, - panel_size, - monitor.scale_factor, - ) -} - /// Center a panel on the monitor's work area (used for Settings windows). pub(super) fn centered_position(monitor: &MonitorPlacement, panel_size: &PanelSize) -> (i32, i32) { let scale = monitor.scale_factor; @@ -186,36 +173,6 @@ pub(super) fn tray_anchor_rect(anchor: crate::state::TrayAnchor) -> Rect { } } -pub(super) fn monitor_placement_for_anchor( - monitors: &[MonitorPlacement], - anchor: crate::state::TrayAnchor, -) -> Option { - // Tray icon dimensions are small pixel counts, far below i32::MAX. - #[expect( - clippy::cast_possible_wrap, - reason = "tray icon pixel dimensions fit in i32" - )] - let anchor_cx = anchor.x + anchor.width as i32 / 2; - #[expect( - clippy::cast_possible_wrap, - reason = "tray icon pixel dimensions fit in i32" - )] - let anchor_cy = anchor.y + anchor.height as i32 / 2; - - monitor_placement_containing_point(monitors, anchor_cx, anchor_cy) -} - -pub(super) fn monitor_placement_containing_point( - monitors: &[MonitorPlacement], - x: i32, - y: i32, -) -> Option { - monitors - .iter() - .find(|monitor| point_in_rect(&monitor.bounds, x, y)) - .copied() -} - pub(super) fn monitor_for_anchor( monitors: &[tauri::Monitor], anchor: crate::state::TrayAnchor, diff --git a/apps/desktop-tauri/src-tauri/src/shell/position.rs b/apps/desktop-tauri/src-tauri/src/shell/position.rs index f5e8379950c..60c04fecef9 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/position.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/position.rs @@ -10,9 +10,8 @@ use crate::surface::SurfaceMode; use crate::window_positioner; use super::geometry::{ - MonitorPlacement, inferred_tray_anchor_rect, monitor_for_anchor, monitor_placement, - monitor_placement_containing_point, monitor_placement_for_anchor, monitor_work_area_rect, - point_in_rect, popout_position, surface_panel_size, tray_anchor_rect, tray_panel_size, + MonitorPlacement, monitor_for_anchor, monitor_placement, monitor_work_area_rect, point_in_rect, + surface_panel_size, tray_anchor_rect, tray_panel_size, }; pub fn inferred_tray_panel_position(app: &AppHandle) -> Option<(i32, i32)> { @@ -44,102 +43,13 @@ fn current_tray_anchor(app: &AppHandle) -> Option { st.lock().ok()?.tray_anchor } -fn visible_surface_position_for_mode(app: &AppHandle, mode: SurfaceMode) -> Option<(i32, i32)> { - let window = app.get_webview_window("main")?; - let monitor_placements = window - .available_monitors() - .ok() - .map(|monitors| monitors.iter().map(monitor_placement).collect::>()); - let current_monitor = window - .current_monitor() - .ok() - .flatten() - .map(|monitor| monitor_placement(&monitor)); - let current_window_bounds = match (window.outer_position(), window.outer_size()) { - (Ok(position), Ok(size)) => Some(((position.x, position.y), (size.width, size.height))), - _ => None, - }; - let primary_monitor = window - .primary_monitor() - .ok() - .flatten() - .map(|monitor| monitor_placement(&monitor)); - - visible_surface_position_for_mode_with_fallbacks( - mode, - monitor_placements.as_deref(), - current_tray_anchor(app), - current_monitor, - current_window_bounds, - primary_monitor, - ) -} - -pub(super) fn visible_surface_position_for_mode_with_fallbacks( - mode: SurfaceMode, - monitor_placements: Option<&[MonitorPlacement]>, - tray_anchor: Option, - current_monitor: Option, - current_window_bounds: Option<((i32, i32), (u32, u32))>, - primary_monitor: Option, -) -> Option<(i32, i32)> { - let panel_size = surface_panel_size(mode); - - if let Some(anchor) = tray_anchor - && let Some(monitors) = monitor_placements - && let Some(monitor) = monitor_placement_for_anchor(monitors, anchor) - { - return Some(popout_position( - Some(&tray_anchor_rect(anchor)), - &monitor, - &panel_size, - )); - } - - // No usable tray anchor (e.g. a right-click menu "Pop Out Dashboard" with no - // prior left-click, or a proof/automation launch). The tray icon lives on - // the primary (taskbar) monitor, so anchor the surface there. Crucially, do - // NOT fall through to the hidden main window's `current_monitor`: after a - // previous session left the surface on a now-off-view monitor, that monitor - // becomes `current_monitor` and the surface would reopen where the user - // can't see it — the multi-monitor "nothing happens" bug. - if tray_anchor.is_none() - && let Some(monitor) = primary_monitor - { - return Some(popout_position( - Some(&inferred_tray_anchor_rect(&monitor)), - &monitor, - &panel_size, - )); - } - - if let Some(monitor) = current_monitor { - return Some(popout_position(None, &monitor, &panel_size)); - } - - if let Some(monitors) = monitor_placements - && let Some((current_top_left, current_size)) = current_window_bounds - && let Some(monitor) = monitor_placement_containing_point( - monitors, - current_top_left.0 + current_size.0 as i32 / 2, - current_top_left.1 + current_size.1 as i32 / 2, - ) - { - return Some(popout_position(None, &monitor, &panel_size)); - } - - let monitor = primary_monitor?; - Some(popout_position(None, &monitor, &panel_size)) -} - pub fn default_surface_position(app: &AppHandle, mode: SurfaceMode) -> Option<(i32, i32)> { match mode { SurfaceMode::Hidden => None, SurfaceMode::TrayPanel => tray_panel_position(app) .or_else(|| inferred_tray_panel_position(app)) .or_else(|| shortcut_panel_position(app)), - SurfaceMode::PopOut => remembered_surface_position(app, mode) - .or_else(|| visible_surface_position_for_mode(app, mode)), + SurfaceMode::PopOut => remembered_surface_position(app, mode), SurfaceMode::Settings => { remembered_surface_position(app, mode).or_else(|| centered_settings_position(app)) } diff --git a/apps/desktop-tauri/src-tauri/src/shell/tests.rs b/apps/desktop-tauri/src-tauri/src/shell/tests.rs index 80ddeb50ccc..2675d22e668 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/tests.rs @@ -1,12 +1,8 @@ use super::ShellTransitionRequest; use super::geometry::{ MonitorPlacement, inferred_tray_anchor_rect, inferred_tray_panel_position_for_monitor, - surface_panel_size, tray_anchor_rect, -}; -use super::position::{ - remembered_panel_size, remembered_surface_position_with_monitors, - visible_surface_position_for_mode_with_fallbacks, }; +use super::position::{remembered_panel_size, remembered_surface_position_with_monitors}; use super::transition::{ SurfaceSnapshot, TransitionResolution, hidden_surface_snapshot, monitor_for_preserved_visible_position, reclamp_preserved_visible_position, @@ -368,247 +364,6 @@ fn preserved_visible_monitor_prefers_top_left_for_straddling_window() { assert_eq!(selected.1, 1.0); } -#[test] -fn visible_surface_position_falls_back_to_current_monitor_without_available_monitors() { - let current_monitor = MonitorPlacement { - bounds: Rect { - x: 1920, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 1920, - y: 0, - width: 1920, - height: 1080, - }, - scale_factor: 1.25, - }; - let primary_monitor = MonitorPlacement { - bounds: Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - scale_factor: 1.0, - }; - let anchor = crate::state::TrayAnchor { - x: 10, - y: 10, - width: 16, - height: 16, - }; - - let position = visible_surface_position_for_mode_with_fallbacks( - SurfaceMode::PopOut, - None, - Some(anchor), - Some(current_monitor), - Some(((2000, 120), (600, 700))), - Some(primary_monitor), - ); - - assert_eq!( - position, - Some(window_positioner::calculate_popout_position( - None, - ¤t_monitor.work_area, - &surface_panel_size(SurfaceMode::PopOut), - current_monitor.scale_factor, - )) - ); -} - -#[test] -fn visible_surface_position_without_anchor_prefers_primary_over_offview_current_monitor() { - // Regression: the hidden main window is parked on a secondary monitor at - // negative coordinates (real machine: DISPLAY5 at x -2048..0). With no tray - // anchor (right-click menu / proof launch), the surface must open on the - // primary (tray/taskbar) monitor — not the off-view secondary, which is the - // "Pop Out Dashboard does nothing" bug. - let offview_current = MonitorPlacement { - bounds: Rect { - x: -2048, - y: 0, - width: 2048, - height: 1152, - }, - work_area: Rect { - x: -2048, - y: 0, - width: 2048, - height: 1104, - }, - scale_factor: 1.0, - }; - let primary = MonitorPlacement { - bounds: Rect { - x: 0, - y: 0, - width: 3413, - height: 1440, - }, - work_area: Rect { - x: 0, - y: 0, - width: 3413, - height: 1392, - }, - scale_factor: 1.0, - }; - - let position = visible_surface_position_for_mode_with_fallbacks( - SurfaceMode::PopOut, - Some(&[offview_current, primary]), - None, // no tray anchor - Some(offview_current), // hidden main window parked off-view - Some(((-1288, 8), (1024, 1088))), // last bounds also off-view - Some(primary), - ) - .expect("should resolve a position"); - - // Must land on the primary monitor (x >= 0), never the negative secondary. - assert!( - position.0 >= 0, - "expected on primary monitor, got {position:?}" - ); - assert_eq!( - position, - window_positioner::calculate_popout_position( - Some(&inferred_tray_anchor_rect(&primary)), - &primary.work_area, - &surface_panel_size(SurfaceMode::PopOut), - primary.scale_factor, - ) - ); -} - -#[test] -fn visible_surface_position_anchor_lookup_uses_monitor_bounds() { - let anchor_monitor = MonitorPlacement { - bounds: Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 0, - y: 0, - width: 1920, - height: 1040, - }, - scale_factor: 1.0, - }; - let current_monitor = MonitorPlacement { - bounds: Rect { - x: 1920, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 1920, - y: 0, - width: 1920, - height: 1080, - }, - scale_factor: 1.25, - }; - let anchor = crate::state::TrayAnchor { - x: 1800, - y: 1040, - width: 24, - height: 24, - }; - - let position = visible_surface_position_for_mode_with_fallbacks( - SurfaceMode::PopOut, - Some(&[anchor_monitor, current_monitor]), - Some(anchor), - Some(current_monitor), - None, - None, - ); - - assert_eq!( - position, - Some(window_positioner::calculate_popout_position( - Some(&tray_anchor_rect(anchor)), - &anchor_monitor.work_area, - &surface_panel_size(SurfaceMode::PopOut), - anchor_monitor.scale_factor, - )) - ); -} - -#[test] -fn visible_surface_position_settings_surface_uses_tray_anchor_position_when_available() { - let anchor_monitor = MonitorPlacement { - bounds: Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 0, - y: 0, - width: 1920, - height: 1040, - }, - scale_factor: 1.0, - }; - let current_monitor = MonitorPlacement { - bounds: Rect { - x: 1920, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 1920, - y: 0, - width: 1920, - height: 1080, - }, - scale_factor: 1.25, - }; - let anchor = crate::state::TrayAnchor { - x: 1800, - y: 1040, - width: 24, - height: 24, - }; - - let position = visible_surface_position_for_mode_with_fallbacks( - SurfaceMode::Settings, - Some(&[anchor_monitor, current_monitor]), - Some(anchor), - Some(current_monitor), - None, - None, - ); - - assert_eq!( - position, - Some(window_positioner::calculate_popout_position( - Some(&tray_anchor_rect(anchor)), - &anchor_monitor.work_area, - &surface_panel_size(SurfaceMode::Settings), - anchor_monitor.scale_factor, - )) - ); -} - #[test] fn inferred_tray_anchor_defaults_to_bottom_right_of_work_area() { let monitor = MonitorPlacement { diff --git a/apps/desktop-tauri/src-tauri/src/window_positioner.rs b/apps/desktop-tauri/src-tauri/src/window_positioner.rs index 407074ed489..f8c4e7d5334 100644 --- a/apps/desktop-tauri/src-tauri/src/window_positioner.rs +++ b/apps/desktop-tauri/src-tauri/src/window_positioner.rs @@ -208,54 +208,6 @@ pub fn calculate_shortcut_position( (x, y) } -/// Calculate detached popout/settings placement. -/// -/// Placement rules: -/// - With a known tray anchor, centre horizontally on the tray parity anchor -/// point (icon centre-x, top-y) and pick above or below based on available -/// space before clamping inside the work area. -/// - Without a tray anchor, fall back to the bottom-right corner of the work -/// area and clamp so the window remains visible. -pub fn calculate_popout_position( - icon_rect: Option<&Rect>, - monitor_rect: &Rect, - panel_size: &PanelSize, - scale_factor: f64, -) -> (i32, i32) { - let (pw, ph) = physical_panel_size(panel_size, scale_factor); - let mx = monitor_rect.x; - let my = monitor_rect.y; - // Monitor dimensions are physical pixels, bounded well below i32::MAX. - #[expect( - clippy::cast_possible_wrap, - reason = "monitor pixel dimensions fit in i32" - )] - let mw = monitor_rect.width as i32; - #[expect( - clippy::cast_possible_wrap, - reason = "monitor pixel dimensions fit in i32" - )] - let mh = monitor_rect.height as i32; - - let (target_x, target_y) = if let Some(icon_rect) = icon_rect { - let space_above = icon_rect.y - my - MARGIN; - let space_below = my + mh - icon_rect.y - MARGIN; - let open_above = space_above >= ph + GAP || space_above > space_below; - calculate_anchored_position( - icon_rect, - monitor_rect, - panel_size, - scale_factor, - icon_rect.y, - open_above, - ) - } else { - (mx + mw - pw - MARGIN, my + mh - ph - MARGIN) - }; - - clamp_position_to_work_area(target_x, target_y, monitor_rect, panel_size, scale_factor) -} - #[cfg(test)] mod tests { use super::*; @@ -285,15 +237,6 @@ mod tests { } } - fn tall_monitor() -> Rect { - Rect { - x: 0, - y: 0, - width: 1920, - height: 1400, - } - } - // --- tray-anchor tests --- #[test] @@ -584,119 +527,4 @@ mod tests { <= i32::try_from(monitor.height).unwrap() ); } - - // --- visible-surface popout tests --- - - #[test] - fn anchored_popout_keeps_no_anchor_bottom_right_fallback() { - let work_area = Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }; - let target = calculate_popout_position(None, &work_area, &panel(), 1.0); - assert_eq!(target, (1492, 512)); - } - - #[test] - fn anchored_popout_uses_same_tray_anchor_x_as_panel_positioning() { - let icon = Rect { - x: 1800, - y: 1040, - width: 24, - height: 24, - }; - - let monitor = standard_monitor(); - let (panel_x, _) = calculate_panel_position(&icon, &monitor, &monitor, &panel(), 1.0); - let (popout_x, _) = - calculate_popout_position(Some(&icon), &standard_monitor(), &panel(), 1.0); - - assert_eq!(popout_x, panel_x); - } - - #[test] - fn popout_clamps_inside_monitor_bounds() { - let icon = Rect { - x: 1910, - y: 1040, - width: 24, - height: 24, - }; - let (x, y) = calculate_popout_position(Some(&icon), &standard_monitor(), &panel(), 1.0); - assert!(x >= 8); - assert!(y >= 8); - } - - #[test] - fn tray_anchored_popout_centres_on_known_anchor() { - let icon = Rect { - x: 1800, - y: 1040, - width: 24, - height: 24, - }; - let (x, _) = calculate_popout_position(Some(&icon), &standard_monitor(), &panel(), 1.0); - let icon_cx = icon.x + 12; - let panel_cx = x + 210; - assert!((icon_cx - panel_cx).abs() <= 1); - } - - #[test] - fn popout_prefers_above_tray_when_space_below_is_tight() { - let icon = Rect { - x: 1600, - y: 1040, - width: 24, - height: 24, - }; - let (_, y) = calculate_popout_position(Some(&icon), &standard_monitor(), &panel(), 1.0); - assert!(y < icon.y); - } - - #[test] - fn popout_prefers_below_tray_when_space_above_is_tight() { - let icon = Rect { - x: 1600, - y: 8, - width: 24, - height: 24, - }; - let (_, y) = calculate_popout_position(Some(&icon), &tall_monitor(), &panel(), 1.0); - assert!(y > icon.y); - } - - #[test] - fn top_taskbar_popout_uses_tray_top_anchor_point() { - let icon = Rect { - x: 1600, - y: 8, - width: 24, - height: 24, - }; - let (_, y) = calculate_popout_position(Some(&icon), &tall_monitor(), &panel(), 1.0); - assert_eq!(y, icon.y + GAP); - } - - #[test] - fn popout_high_dpi_uses_physical_panel_size() { - let icon = Rect { - x: 1800, - y: 1040, - width: 24, - height: 24, - }; - let (x1, y1) = calculate_popout_position(Some(&icon), &standard_monitor(), &panel(), 1.0); - let (x2, y2) = calculate_popout_position(Some(&icon), &standard_monitor(), &panel(), 2.0); - - assert!( - x2 < x1, - "higher scale should account for wider physical popout bounds" - ); - assert!( - y2 < y1, - "higher scale should account for taller physical popout bounds" - ); - } } From 3578bc697f697ad7175285ab6ebc1443710d5a58 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:12:30 +0700 Subject: [PATCH 08/22] Drop the never-forced same-mode reopen path and test-only reveal predicate --- .../src-tauri/src/shell/tests.rs | 76 +------------------ .../src-tauri/src/shell/transition.rs | 64 +--------------- 2 files changed, 4 insertions(+), 136 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/shell/tests.rs b/apps/desktop-tauri/src-tauri/src/shell/tests.rs index 2675d22e668..0a79e60d830 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/tests.rs @@ -8,7 +8,7 @@ use super::transition::{ monitor_for_preserved_visible_position, reclamp_preserved_visible_position, recovery_snapshot_for_failed_transition, resolve_transition_position, resolve_transition_request, restore_recovery_surface, restore_surface_snapshot, - should_force_tray_panel_reveal, should_synthesize_default_position, + should_synthesize_default_position, }; use super::window::{logical_size_from_geometry, prepare_hide_to_tray_if_current}; @@ -63,34 +63,6 @@ fn conditional_hide_to_tray_leaves_non_matching_surface_alone() { // dedicated window now (see `shell::flyout_window::toggle_with_blur_consume` // and its own test module in flyout_window.rs). -#[test] -fn tray_reveal_fallback_only_for_hidden_tray_panel() { - assert!(should_force_tray_panel_reveal( - SurfaceMode::TrayPanel, - false, - Some((328, 776)), - )); - assert!(!should_force_tray_panel_reveal( - SurfaceMode::TrayPanel, - true, - Some((328, 776)), - )); - assert!(!should_force_tray_panel_reveal( - SurfaceMode::Hidden, - false, - Some((16, 16)), - )); -} - -#[test] -fn tray_reveal_fallback_recovers_tiny_shell_window() { - assert!(should_force_tray_panel_reveal( - SurfaceMode::TrayPanel, - true, - Some((16, 16)), - )); -} - #[test] fn tray_show_grace_is_based_on_actual_show_time() { let mut state = AppState::new(); @@ -157,7 +129,6 @@ fn same_mode_about_request_resolves_as_retarget() { }, position: None, }, - false, ); match resolution { @@ -188,7 +159,6 @@ fn same_mode_provider_request_resolves_as_retarget() { }, position: None, }, - false, ); match resolution { @@ -205,30 +175,6 @@ fn same_mode_provider_request_resolves_as_retarget() { } } -#[test] -fn same_mode_reopen_request_resolves_as_update() { - let mut state = AppState::new(); - state.transition_surface(SurfaceMode::TrayPanel, SurfaceTarget::Summary); - - let resolution = resolve_transition_request( - &state, - &ShellTransitionRequest { - mode: SurfaceMode::TrayPanel, - target: SurfaceTarget::Summary, - position: Some((10, 20)), - }, - true, - ); - - match resolution { - TransitionResolution::SameModeReopen { mode, target } => { - assert_eq!(mode, SurfaceMode::TrayPanel); - assert_eq!(target, SurfaceTarget::Summary); - } - _ => panic!("expected same-mode reopen update"), - } -} - #[test] fn same_mode_retarget_skips_default_position_synthesis() { let resolution = TransitionResolution::SameModeRetarget { @@ -267,26 +213,6 @@ fn same_mode_retarget_preserves_explicit_position() { assert_eq!(position, Some((10, 20))); } -#[test] -fn same_mode_reopen_still_uses_default_position() { - let resolution = TransitionResolution::SameModeReopen { - mode: SurfaceMode::TrayPanel, - target: SurfaceTarget::Summary, - }; - let mut fallback_called = false; - - let position = resolve_transition_position(None, &resolution, true, || { - fallback_called = true; - Some((42, 24)) - }); - - assert_eq!(position, Some((42, 24))); - assert!( - fallback_called, - "same-mode reopen should still synthesize a default position" - ); -} - #[test] fn visible_mode_change_skips_default_position_synthesis() { let resolution = TransitionResolution::ModeChange { diff --git a/apps/desktop-tauri/src-tauri/src/shell/transition.rs b/apps/desktop-tauri/src-tauri/src/shell/transition.rs index 6b079ebc1b1..19d478b2162 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/transition.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/transition.rs @@ -26,24 +26,6 @@ fn os_position(_window: &WebviewWindow, x: i32, y: i32) -> tauri::PhysicalPositi tauri::PhysicalPosition::new(x, y) } -// `should_force_tray_panel_reveal` is the predicate the (now-removed) -// startup tray-panel reveal fallback used to decide whether to force-show -// a hidden/tiny `main` window. The fallback itself was deleted once `main` -// could no longer transition into `SurfaceMode::TrayPanel` (that mode opens -// as the dedicated `flyout` window — see `shell::flyout_window`). The -// predicate is retained under `#[cfg(test)]` because `shell::tests` still -// exercises it as a pure unit. -#[cfg(test)] -pub(super) fn should_force_tray_panel_reveal( - current: SurfaceMode, - main_window_visible: bool, - main_window_size: Option<(u32, u32)>, -) -> bool { - current == SurfaceMode::TrayPanel - && (!main_window_visible - || main_window_size.is_some_and(|(width, height)| width < 100 || height < 100)) -} - fn mark_tray_panel_shown(app: &AppHandle) { if let Some(state) = app.try_state::>() && let Ok(mut guard) = state.lock() @@ -67,10 +49,6 @@ pub(super) enum TransitionResolution { mode: SurfaceMode, target: SurfaceTarget, }, - SameModeReopen { - mode: SurfaceMode, - target: SurfaceTarget, - }, Noop { mode: SurfaceMode, }, @@ -85,31 +63,20 @@ pub fn transition_to_target( position: Option<(i32, i32)>, activation: Activation, ) -> Result { - apply_transition_request_with_strategy( + apply_transition_request( app, ShellTransitionRequest { mode, target, position, }, - false, activation, ) } -fn apply_transition_request_with_strategy( - app: &AppHandle, - request: ShellTransitionRequest, - force_same_mode_apply: bool, - activation: Activation, -) -> Result { - apply_transition_request(app, request, force_same_mode_apply, activation) -} - fn apply_transition_request( app: &AppHandle, request: ShellTransitionRequest, - force_same_mode_apply: bool, activation: Activation, ) -> Result { let _transition_guard = SHELL_TRANSITION_SERIAL.lock().unwrap(); @@ -123,7 +90,7 @@ fn apply_transition_request( let (previous, resolution) = { let guard = st.lock().unwrap(); let previous = current_surface_snapshot(&guard); - let resolution = resolve_transition_request(&guard, &request, force_same_mode_apply); + let resolution = resolve_transition_request(&guard, &request); (previous, resolution) }; @@ -147,22 +114,6 @@ fn apply_transition_request( TransitionResolution::SameModeRetarget { mode, target } => { apply_same_mode_target_update(app, &window, mode, target, position, activation) } - TransitionResolution::SameModeReopen { mode, target } => { - let transition = SurfaceTransition { - from: mode, - to: mode, - properties: mode.window_properties(), - }; - apply_transition( - app, - &window, - &transition, - &previous, - target, - position, - activation, - ) - } TransitionResolution::Noop { mode } => Ok(mode), } } @@ -170,7 +121,6 @@ fn apply_transition_request( pub(super) fn resolve_transition_request( state: &AppState, request: &ShellTransitionRequest, - force_same_mode_apply: bool, ) -> TransitionResolution { let mode = state.surface_machine.current(); let target = AppState::resolved_target_for_mode(request.mode, Some(request.target.clone())); @@ -186,8 +136,6 @@ pub(super) fn resolve_transition_request( } } else if state.current_target != target { TransitionResolution::SameModeRetarget { mode, target } - } else if force_same_mode_apply { - TransitionResolution::SameModeReopen { mode, target } } else { TransitionResolution::Noop { mode } } @@ -214,13 +162,8 @@ where } match resolution { - TransitionResolution::ModeChange { .. } | TransitionResolution::SameModeReopen { .. } - if synthesize_default => - { - fallback() - } + TransitionResolution::ModeChange { .. } if synthesize_default => fallback(), TransitionResolution::ModeChange { .. } - | TransitionResolution::SameModeReopen { .. } | TransitionResolution::SameModeRetarget { .. } | TransitionResolution::Noop { .. } => None, } @@ -374,7 +317,6 @@ pub(super) fn should_synthesize_default_position(resolution: &TransitionResoluti || (transition.from == SurfaceMode::TrayPanel && transition.to == SurfaceMode::Settings) } - TransitionResolution::SameModeReopen { .. } => true, TransitionResolution::SameModeRetarget { .. } | TransitionResolution::Noop { .. } => false, } } From 00f45e2f5e413cf86cb6273acef5517c3ea170f9 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:12:30 +0700 Subject: [PATCH 09/22] Remove the never-armed startup tray blur grace --- apps/desktop-tauri/src-tauri/src/main.rs | 8 -------- .../desktop-tauri/src-tauri/src/shell/flyout_window.rs | 5 +---- apps/desktop-tauri/src-tauri/src/state.rs | 10 ---------- 3 files changed, 1 insertion(+), 22 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/main.rs b/apps/desktop-tauri/src-tauri/src/main.rs index d3f9cbd4d99..f63f9a9f4a2 100644 --- a/apps/desktop-tauri/src-tauri/src/main.rs +++ b/apps/desktop-tauri/src-tauri/src/main.rs @@ -382,14 +382,6 @@ fn main() { ) { return; } - if let Some(st) = window.app_handle().try_state::>() - && st - .lock() - .unwrap() - .take_startup_tray_blur_grace(std::time::Instant::now()) - { - return; - } // Grace period: ignore blur within 500ms of showing the panel. // On Windows, the tray click can cause a spurious blur before // the window fully acquires focus. diff --git a/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs b/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs index 1b177f799a2..b55402b76c6 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs @@ -340,10 +340,7 @@ pub fn handle_window_event(window: &tauri::Window, event: &tauri::WindowEvent) - return true; }; { - let mut guard = st.lock().unwrap(); - if guard.take_startup_tray_blur_grace(Instant::now()) { - return true; - } + let guard = st.lock().unwrap(); if guard.was_tray_panel_recently_shown(Instant::now(), RECENTLY_SHOWN_GRACE) { return true; } diff --git a/apps/desktop-tauri/src-tauri/src/state.rs b/apps/desktop-tauri/src-tauri/src/state.rs index 07191d57c48..1309183c205 100644 --- a/apps/desktop-tauri/src-tauri/src/state.rs +++ b/apps/desktop-tauri/src-tauri/src/state.rs @@ -161,9 +161,6 @@ pub struct AppState { /// Instant when focus loss last dismissed the tray panel. The following /// tray click consumes this marker instead of reopening the panel. pub last_blur_dismissed_at: Option, - /// One-shot grace for a blur event caused while revealing the tray panel - /// during explicit startup. - pub startup_tray_blur_grace_until: Option, /// One-shot permission for frontend layout code to reveal a newly opened /// flyout, carrying whether the revealed window may take focus. pub flyout_reveal_pending: Option, @@ -223,7 +220,6 @@ impl AppState { notification_manager: codexbar::notifications::NotificationManager::new(), last_shown_at: None, last_blur_dismissed_at: None, - startup_tray_blur_grace_until: None, flyout_reveal_pending: None, gesture_blur_guard: None, auto_resume: crate::auto_resume::AutoResumeState::default(), @@ -257,12 +253,6 @@ impl AppState { .is_some_and(|dismissed_at| now.duration_since(dismissed_at) <= max_age) } - pub fn take_startup_tray_blur_grace(&mut self, now: std::time::Instant) -> bool { - self.startup_tray_blur_grace_until - .take() - .is_some_and(|until| now <= until) - } - pub fn arm_flyout_reveal(&mut self, activation: Activation) { self.flyout_reveal_pending = Some(activation); } From 067b1182e3c8c312de28eaeba46ed77994486eb8 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:12:30 +0700 Subject: [PATCH 10/22] Remove the unused SurfaceTarget::parse --- .../src-tauri/src/surface_target.rs | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/surface_target.rs b/apps/desktop-tauri/src-tauri/src/surface_target.rs index b80325bd3a3..fe03474cf97 100644 --- a/apps/desktop-tauri/src-tauri/src/surface_target.rs +++ b/apps/desktop-tauri/src-tauri/src/surface_target.rs @@ -32,36 +32,6 @@ pub enum SurfaceTarget { } impl SurfaceTarget { - #[allow( - dead_code, - reason = "surface target type reserved for future window management integration" - )] - pub fn parse(s: &str) -> Option { - match s { - "summary" => Some(Self::Summary), - "dashboard" => Some(Self::Dashboard), - _ => { - if let Some(provider_id) = s.strip_prefix("provider:") - && !provider_id.is_empty() - { - return Some(Self::Provider { - provider_id: provider_id.to_string(), - }); - } - - if let Some(tab) = s.strip_prefix("settings:") - && !tab.is_empty() - { - return Some(Self::Settings { - tab: tab.to_string(), - }); - } - - None - } - } - } - pub fn default_for_mode(mode: SurfaceMode) -> Self { match mode { SurfaceMode::Hidden | SurfaceMode::TrayPanel => Self::Summary, @@ -90,28 +60,6 @@ mod tests { use super::{SurfaceTarget, is_supported_settings_tab}; use serde_json::json; - #[test] - fn parse_provider_target() { - let target = SurfaceTarget::parse("provider:codex").unwrap(); - assert_eq!( - target, - SurfaceTarget::Provider { - provider_id: "codex".into() - } - ); - } - - #[test] - fn parse_settings_about_target() { - let target = SurfaceTarget::parse("settings:about").unwrap(); - assert_eq!( - target, - SurfaceTarget::Settings { - tab: "about".into() - } - ); - } - #[test] fn serialize_provider_target_for_bridge() { let value = serde_json::to_value(SurfaceTarget::Provider { From 1bed3c8afb679f0f5c7ca6ac2f0279a5075f4505 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:17:34 +0700 Subject: [PATCH 11/22] Add Rect helpers and safe_scale, and share the WindowGeometry impls --- .../src-tauri/src/floatbar/placement.rs | 6 +- .../src-tauri/src/floatbar/window.rs | 106 +++---- .../src-tauri/src/shell/flyout_placement.rs | 14 +- .../src-tauri/src/shell/flyout_window.rs | 19 +- .../src-tauri/src/shell/geometry.rs | 78 ++--- .../src-tauri/src/shell/tests.rs | 118 +------- .../src-tauri/src/shell/window.rs | 6 +- .../src-tauri/src/tray_bridge.rs | 12 +- .../src-tauri/src/window_positioner.rs | 273 +++++------------- 9 files changed, 159 insertions(+), 473 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/floatbar/placement.rs b/apps/desktop-tauri/src-tauri/src/floatbar/placement.rs index fe3f1a8dc28..e6c10344451 100644 --- a/apps/desktop-tauri/src-tauri/src/floatbar/placement.rs +++ b/apps/desktop-tauri/src-tauri/src/floatbar/placement.rs @@ -64,11 +64,7 @@ impl MonitorLayout { } fn scale_milli(scale_factor: f64) -> u32 { - if scale_factor.is_finite() && scale_factor > 0.0 { - (scale_factor * 1000.0).round() as u32 - } else { - 1000 - } + (crate::window_positioner::safe_scale(scale_factor) * 1000.0).round() as u32 } #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/apps/desktop-tauri/src-tauri/src/floatbar/window.rs b/apps/desktop-tauri/src-tauri/src/floatbar/window.rs index 1c3289f6e1c..dacafb9a8d7 100644 --- a/apps/desktop-tauri/src-tauri/src/floatbar/window.rs +++ b/apps/desktop-tauri/src-tauri/src/floatbar/window.rs @@ -92,11 +92,7 @@ fn fallback_physical_position( scale_factor: f64, style: &str, ) -> PhysicalPosition { - let scale_factor = if scale_factor.is_finite() && scale_factor > 0.0 { - scale_factor - } else { - 1.0 - }; + let scale_factor = crate::window_positioner::safe_scale(scale_factor); let mon_x = work_area_position.x as f64 / scale_factor; let mon_y = work_area_position.y as f64 / scale_factor; let mon_w = work_area_size.width as f64 / scale_factor; @@ -458,70 +454,46 @@ pub trait WindowGeometry { fn set_physical_position(&self, position: PhysicalPosition) -> tauri::Result<()>; } -impl WindowGeometry for tauri::WebviewWindow { - fn outer_position(&self) -> tauri::Result> { - tauri::WebviewWindow::outer_position(self) - } - fn outer_size(&self) -> tauri::Result> { - tauri::WebviewWindow::outer_size(self) - } - fn scale_factor(&self) -> tauri::Result { - tauri::WebviewWindow::scale_factor(self) - } - fn is_minimized(&self) -> tauri::Result { - tauri::WebviewWindow::is_minimized(self) - } - fn primary_monitor(&self) -> tauri::Result> { - tauri::WebviewWindow::primary_monitor(self) - } - fn available_monitors(&self) -> tauri::Result> { - tauri::WebviewWindow::available_monitors(self) - } - fn restore_without_activation(&self) -> tauri::Result<()> { - // Queued on the main thread ahead of the calls below, so the restore - // lands before `unminimize` re-reads the state and before the caller - // moves the window. - let window = self.clone(); - tauri::WebviewWindow::run_on_main_thread(self, move || { - crate::shell::activation::restore_minimized_without_activation(&window); - })?; - tauri::WebviewWindow::unminimize(self) - } - fn set_physical_position(&self, position: PhysicalPosition) -> tauri::Result<()> { - tauri::WebviewWindow::set_position(self, position) - } +macro_rules! impl_window_geometry { + ($ty:ident) => { + impl WindowGeometry for tauri::$ty { + fn outer_position(&self) -> tauri::Result> { + tauri::$ty::outer_position(self) + } + fn outer_size(&self) -> tauri::Result> { + tauri::$ty::outer_size(self) + } + fn scale_factor(&self) -> tauri::Result { + tauri::$ty::scale_factor(self) + } + fn is_minimized(&self) -> tauri::Result { + tauri::$ty::is_minimized(self) + } + fn primary_monitor(&self) -> tauri::Result> { + tauri::$ty::primary_monitor(self) + } + fn available_monitors(&self) -> tauri::Result> { + tauri::$ty::available_monitors(self) + } + fn restore_without_activation(&self) -> tauri::Result<()> { + // Queued on the main thread ahead of the calls below, so the + // restore lands before `unminimize` re-reads the state and + // before the caller moves the window. + let window = self.clone(); + tauri::$ty::run_on_main_thread(self, move || { + crate::shell::activation::restore_minimized_without_activation(&window); + })?; + tauri::$ty::unminimize(self) + } + fn set_physical_position(&self, position: PhysicalPosition) -> tauri::Result<()> { + tauri::$ty::set_position(self, position) + } + } + }; } -impl WindowGeometry for tauri::Window { - fn outer_position(&self) -> tauri::Result> { - tauri::Window::outer_position(self) - } - fn outer_size(&self) -> tauri::Result> { - tauri::Window::outer_size(self) - } - fn scale_factor(&self) -> tauri::Result { - tauri::Window::scale_factor(self) - } - fn is_minimized(&self) -> tauri::Result { - tauri::Window::is_minimized(self) - } - fn primary_monitor(&self) -> tauri::Result> { - tauri::Window::primary_monitor(self) - } - fn available_monitors(&self) -> tauri::Result> { - tauri::Window::available_monitors(self) - } - fn restore_without_activation(&self) -> tauri::Result<()> { - let window = self.clone(); - tauri::Window::run_on_main_thread(self, move || { - crate::shell::activation::restore_minimized_without_activation(&window); - })?; - tauri::Window::unminimize(self) - } - fn set_physical_position(&self, position: PhysicalPosition) -> tauri::Result<()> { - tauri::Window::set_position(self, position) - } -} +impl_window_geometry!(WebviewWindow); +impl_window_geometry!(Window); /// Resize the floatbar to the given logical dimensions and re-assert the /// native interaction invariants in the same step. diff --git a/apps/desktop-tauri/src-tauri/src/shell/flyout_placement.rs b/apps/desktop-tauri/src-tauri/src/shell/flyout_placement.rs index 30df73a6666..c6d0d059aae 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/flyout_placement.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/flyout_placement.rs @@ -46,12 +46,7 @@ pub fn clear_position() { pub fn placed_position(window: &tauri::WebviewWindow) -> Option<(i32, i32)> { let (x, y) = stored_position()?; let outer = window.outer_size().ok()?; - let panel = Rect { - x, - y, - width: outer.width, - height: outer.height, - }; + let panel = Rect::new(x, y, outer.width, outer.height); let work_areas: Vec = window .available_monitors() .ok()? @@ -435,12 +430,7 @@ mod tests { } fn area(x: i32, y: i32, width: u32, height: u32) -> Rect { - Rect { - x, - y, - width, - height, - } + Rect::new(x, y, width, height) } // A stacked layout: primary monitor below, a taller one above. diff --git a/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs b/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs index b55402b76c6..6c13a33e705 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/flyout_window.rs @@ -464,12 +464,12 @@ pub fn reanchor(app: &AppHandle) -> Result<(), String> { let work_area = crate::shell::geometry::monitor_work_area_rect(&monitor); // The target monitor can differ from the window's previous monitor. let scale = monitor.scale_factor().max(1.0); - let monitor_bounds = Rect { - x: monitor.position().x, - y: monitor.position().y, - width: monitor.size().width, - height: monitor.size().height, - }; + let monitor_bounds = Rect::new( + monitor.position().x, + monitor.position().y, + monitor.size().width, + monitor.size().height, + ); let (x, y) = { if let Some(cursor) = cursor { @@ -481,12 +481,7 @@ pub fn reanchor(app: &AppHandle) -> Result<(), String> { ) } else if let Some(a) = anchor { crate::window_positioner::calculate_panel_position( - &Rect { - x: a.x, - y: a.y, - width: a.width, - height: a.height, - }, + &Rect::new(a.x, a.y, a.width, a.height), &monitor_bounds, &work_area, &panel_size, diff --git a/apps/desktop-tauri/src-tauri/src/shell/geometry.rs b/apps/desktop-tauri/src-tauri/src/shell/geometry.rs index ed574f72eb7..0aa1967a56a 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/geometry.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/geometry.rs @@ -48,21 +48,16 @@ pub(super) fn monitor_work_area_rect(monitor: &tauri::Monitor) -> Rect { && area.x + area.width <= position.x + size_width && area.y + area.height <= position.y + size_height { - return Rect { - x: area.x, - y: area.y, - width: area.width as u32, - height: area.height as u32, - }; + return Rect::new(area.x, area.y, area.width as u32, area.height as u32); } let work_area = monitor.work_area(); - Rect { - x: work_area.position.x, - y: work_area.position.y, - width: work_area.size.width, - height: work_area.size.height, - } + Rect::new( + work_area.position.x, + work_area.position.y, + work_area.size.width, + work_area.size.height, + ) } pub(super) fn monitor_placement(monitor: &tauri::Monitor) -> MonitorPlacement { @@ -70,12 +65,7 @@ pub(super) fn monitor_placement(monitor: &tauri::Monitor) -> MonitorPlacement { let size = monitor.size(); MonitorPlacement { - bounds: Rect { - x: position.x, - y: position.y, - width: size.width, - height: size.height, - }, + bounds: Rect::new(position.x, position.y, size.width, size.height), work_area: monitor_work_area_rect(monitor), scale_factor: monitor.scale_factor(), } @@ -90,11 +80,8 @@ pub(super) fn centered_position(monitor: &MonitorPlacement, panel_size: &PanelSi #[expect(clippy::cast_possible_truncation, reason = "whole units by design")] let ph = (panel_size.height as f64 * scale) as i32; let wa = &monitor.work_area; - // Work-area dimensions are physical pixels, bounded well below i32::MAX. - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let x = wa.x + (wa.width as i32 - pw) / 2; - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let y = wa.y + (wa.height as i32 - ph) / 2; + let x = wa.x + (wa.signed_width() - pw) / 2; + let y = wa.y + (wa.signed_height() - ph) / 2; (x, y) } @@ -102,17 +89,12 @@ pub(super) fn inferred_tray_anchor_rect(monitor: &MonitorPlacement) -> Rect { const SYNTHETIC_TRAY_ICON_SIZE: u32 = 24; const SYNTHETIC_TRAY_EDGE_PADDING: i32 = 8; - // Monitor bounds and work area are physical pixels, bounded well below i32::MAX. - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let work_right = monitor.work_area.x + monitor.work_area.width as i32; - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let work_bottom = monitor.work_area.y + monitor.work_area.height as i32; + let work_right = monitor.work_area.right(); + let work_bottom = monitor.work_area.bottom(); let bounds_left = monitor.bounds.x; - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let bounds_right = monitor.bounds.x + monitor.bounds.width as i32; + let bounds_right = monitor.bounds.right(); let bounds_top = monitor.bounds.y; - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let bounds_bottom = monitor.bounds.y + monitor.bounds.height as i32; + let bounds_bottom = monitor.bounds.bottom(); let left_gap = monitor.work_area.x - bounds_left; let right_gap = bounds_right - work_right; let top_gap = monitor.work_area.y - bounds_top; @@ -139,12 +121,7 @@ pub(super) fn inferred_tray_anchor_rect(monitor: &MonitorPlacement) -> Rect { bounds_bottom - icon_size - SYNTHETIC_TRAY_EDGE_PADDING }; - Rect { - x, - y, - width: SYNTHETIC_TRAY_ICON_SIZE, - height: SYNTHETIC_TRAY_ICON_SIZE, - } + Rect::new(x, y, SYNTHETIC_TRAY_ICON_SIZE, SYNTHETIC_TRAY_ICON_SIZE) } pub(super) fn inferred_tray_panel_position_for_monitor(monitor: &MonitorPlacement) -> (i32, i32) { @@ -165,12 +142,7 @@ pub(super) fn inferred_tray_panel_position_for_monitor_size( } pub(super) fn tray_anchor_rect(anchor: crate::state::TrayAnchor) -> Rect { - Rect { - x: anchor.x, - y: anchor.y, - width: anchor.width, - height: anchor.height, - } + Rect::new(anchor.x, anchor.y, anchor.width, anchor.height) } pub(super) fn monitor_for_anchor( @@ -200,24 +172,10 @@ pub(super) fn monitor_containing_point( monitors.iter().find(|monitor| { let pos = monitor.position(); let size = monitor.size(); - point_in_rect( - &Rect { - x: pos.x, - y: pos.y, - width: size.width, - height: size.height, - }, - x, - y, - ) + point_in_rect(&Rect::new(pos.x, pos.y, size.width, size.height), x, y) }) } pub(super) fn point_in_rect(rect: &Rect, x: i32, y: i32) -> bool { - // Rect dimensions are physical pixels, bounded well below i32::MAX. - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let right = rect.x + rect.width as i32; - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let bottom = rect.y + rect.height as i32; - x >= rect.x && x < right && y >= rect.y && y < bottom + x >= rect.x && x < rect.right() && y >= rect.y && y < rect.bottom() } diff --git a/apps/desktop-tauri/src-tauri/src/shell/tests.rs b/apps/desktop-tauri/src-tauri/src/shell/tests.rs index 0a79e60d830..f46b89bc20a 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/tests.rs @@ -247,12 +247,7 @@ fn visible_mode_change_skips_default_position_synthesis() { #[test] fn larger_visible_destination_reclamps_preserved_top_left() { let current_top_left = (1492, 512); - let monitor = Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }; + let monitor = Rect::new(0, 0, 1920, 1080); let reclamped = reclamp_preserved_visible_position(current_top_left, &monitor, SurfaceMode::Settings, 1.0); @@ -263,24 +258,8 @@ fn larger_visible_destination_reclamps_preserved_top_left() { #[test] fn preserved_visible_monitor_prefers_top_left_for_straddling_window() { let monitors = vec![ - ( - Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - 1.0, - ), - ( - Rect { - x: 1920, - y: 0, - width: 1920, - height: 1080, - }, - 1.25, - ), + (Rect::new(0, 0, 1920, 1080), 1.0), + (Rect::new(1920, 0, 1920, 1080), 1.25), ]; let selected = monitor_for_preserved_visible_position(&monitors, (1800, 120), Some((600, 700))) @@ -293,18 +272,8 @@ fn preserved_visible_monitor_prefers_top_left_for_straddling_window() { #[test] fn inferred_tray_anchor_defaults_to_bottom_right_of_work_area() { let monitor = MonitorPlacement { - bounds: Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 0, - y: 0, - width: 1920, - height: 1040, - }, + bounds: Rect::new(0, 0, 1920, 1080), + work_area: Rect::new(0, 0, 1920, 1040), scale_factor: 1.0, }; @@ -319,18 +288,8 @@ fn inferred_tray_anchor_defaults_to_bottom_right_of_work_area() { #[test] fn inferred_tray_anchor_supports_top_taskbar_layouts() { let monitor = MonitorPlacement { - bounds: Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 0, - y: 40, - width: 1920, - height: 1040, - }, + bounds: Rect::new(0, 0, 1920, 1080), + work_area: Rect::new(0, 40, 1920, 1040), scale_factor: 1.0, }; @@ -343,18 +302,8 @@ fn inferred_tray_anchor_supports_top_taskbar_layouts() { #[test] fn inferred_tray_anchor_supports_left_taskbar_layouts() { let monitor = MonitorPlacement { - bounds: Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 40, - y: 0, - width: 1880, - height: 1080, - }, + bounds: Rect::new(0, 0, 1920, 1080), + work_area: Rect::new(40, 0, 1880, 1080), scale_factor: 1.0, }; @@ -367,18 +316,8 @@ fn inferred_tray_anchor_supports_left_taskbar_layouts() { #[test] fn inferred_tray_anchor_supports_right_taskbar_layouts() { let monitor = MonitorPlacement { - bounds: Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 0, - y: 0, - width: 1880, - height: 1080, - }, + bounds: Rect::new(0, 0, 1920, 1080), + work_area: Rect::new(0, 0, 1880, 1080), scale_factor: 1.0, }; @@ -391,18 +330,8 @@ fn inferred_tray_anchor_supports_right_taskbar_layouts() { #[test] fn inferred_tray_panel_position_uses_tray_style_corner_fallback() { let monitor = MonitorPlacement { - bounds: Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - }, - work_area: Rect { - x: 0, - y: 0, - width: 1920, - height: 1040, - }, + bounds: Rect::new(0, 0, 1920, 1080), + work_area: Rect::new(0, 0, 1920, 1040), scale_factor: 1.0, }; @@ -411,12 +340,7 @@ fn inferred_tray_panel_position_uses_tray_style_corner_fallback() { assert_eq!( position, window_positioner::calculate_panel_position( - &Rect { - x: 1888, - y: 1048, - width: 24, - height: 24, - }, + &Rect::new(1888, 1048, 24, 24), &monitor.bounds, &monitor.work_area, &super::geometry::tray_panel_size(), @@ -562,18 +486,8 @@ fn hidden_surface_snapshot_matches_non_visible_shell_state() { #[test] fn remembered_popout_position_clamps_using_stored_size() { let monitor = MonitorPlacement { - bounds: Rect { - x: 0, - y: 0, - width: 1000, - height: 800, - }, - work_area: Rect { - x: 0, - y: 0, - width: 1000, - height: 800, - }, + bounds: Rect::new(0, 0, 1000, 800), + work_area: Rect::new(0, 0, 1000, 800), scale_factor: 1.0, }; let stored = crate::geometry_store::StoredGeometry { diff --git a/apps/desktop-tauri/src-tauri/src/shell/window.rs b/apps/desktop-tauri/src-tauri/src/shell/window.rs index 470e8c0187c..5c25c45a30a 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/window.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/window.rs @@ -139,11 +139,7 @@ fn capped_logical_size(window: &WebviewWindow, width: f64, height: f64) -> (f64, }; let scale = monitor.scale_factor(); - let scale = if scale.is_finite() && scale > 0.0 { - scale - } else { - 1.0 - }; + let scale = crate::window_positioner::safe_scale(scale); let work_area = monitor.work_area(); let max_width = (work_area.size.width as f64 / scale - MARGIN).max(320.0); let max_height = (work_area.size.height as f64 / scale - MARGIN).max(240.0); diff --git a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs index 11dfc7d1758..0f09e4d5546 100644 --- a/apps/desktop-tauri/src-tauri/src/tray_bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/tray_bridge.rs @@ -26,11 +26,7 @@ struct MonitorScaleInfo { impl MonitorScaleInfo { fn from_monitor(monitor: &tauri::Monitor) -> Self { let scale_factor = monitor.scale_factor(); - let safe_scale = if scale_factor.is_finite() && scale_factor > 0.0 { - scale_factor - } else { - 1.0 - }; + let safe_scale = crate::window_positioner::safe_scale(scale_factor); let position = monitor.position(); let size = monitor.size(); @@ -63,11 +59,7 @@ fn logical_to_physical_anchor( height: f64, scale_factor: f64, ) -> TrayAnchor { - let safe_scale = if scale_factor.is_finite() && scale_factor > 0.0 { - scale_factor - } else { - 1.0 - }; + let safe_scale = crate::window_positioner::safe_scale(scale_factor); TrayAnchor { x: (x * safe_scale).round() as i32, diff --git a/apps/desktop-tauri/src-tauri/src/window_positioner.rs b/apps/desktop-tauri/src-tauri/src/window_positioner.rs index f8c4e7d5334..7ccb842c07e 100644 --- a/apps/desktop-tauri/src-tauri/src/window_positioner.rs +++ b/apps/desktop-tauri/src-tauri/src/window_positioner.rs @@ -1,9 +1,3 @@ -// Public positioning API — consumed by the shell and tested here. -#![allow( - dead_code, - reason = "window positioner types reserved for future window management integration" -)] - /// A rectangle in physical pixels (monitor work area or icon bounds). #[derive(Debug, Clone, Copy)] pub struct Rect { @@ -13,6 +7,45 @@ pub struct Rect { pub height: u32, } +impl Rect { + pub const fn new(x: i32, y: i32, width: u32, height: u32) -> Self { + Self { + x, + y, + width, + height, + } + } + + // Physical pixel extents are bounded well below i32::MAX. + #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] + pub const fn signed_width(&self) -> i32 { + self.width as i32 + } + + #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] + pub const fn signed_height(&self) -> i32 { + self.height as i32 + } + + pub const fn right(&self) -> i32 { + self.x + self.signed_width() + } + + pub const fn bottom(&self) -> i32 { + self.y + self.signed_height() + } +} + +/// Substitute 1.0 for a non-finite or non-positive monitor scale factor. +pub fn safe_scale(scale_factor: f64) -> f64 { + if scale_factor.is_finite() && scale_factor > 0.0 { + scale_factor + } else { + 1.0 + } +} + /// Panel dimensions in logical pixels. #[derive(Debug, Clone, Copy)] pub struct PanelSize { @@ -25,11 +58,7 @@ const MARGIN: i32 = 8; const GAP: i32 = 8; fn physical_panel_size(panel_size: &PanelSize, scale_factor: f64) -> (i32, i32) { - let scale_factor = if scale_factor.is_finite() && scale_factor > 0.0 { - scale_factor - } else { - 1.0 - }; + let scale_factor = safe_scale(scale_factor); // Physical size is rounded to whole pixels before truncation. #[expect(clippy::cast_possible_truncation, reason = "whole units by design")] @@ -39,7 +68,7 @@ fn physical_panel_size(panel_size: &PanelSize, scale_factor: f64) -> (i32, i32) (width, height) } -fn clamp_to_work_area( +pub fn clamp_position_to_work_area( target_x: i32, target_y: i32, monitor_rect: &Rect, @@ -49,17 +78,8 @@ fn clamp_to_work_area( let (pw, ph) = physical_panel_size(panel_size, scale_factor); let min_x = monitor_rect.x + MARGIN; let min_y = monitor_rect.y + MARGIN; - // Monitor dimensions are physical pixels, bounded well below i32::MAX. - #[expect( - clippy::cast_possible_wrap, - reason = "monitor pixel dimensions fit in i32" - )] - let max_x = (monitor_rect.x + monitor_rect.width as i32 - pw - MARGIN).max(min_x); - #[expect( - clippy::cast_possible_wrap, - reason = "monitor pixel dimensions fit in i32" - )] - let max_y = (monitor_rect.y + monitor_rect.height as i32 - ph - MARGIN).max(min_y); + let max_x = (monitor_rect.right() - pw - MARGIN).max(min_x); + let max_y = (monitor_rect.bottom() - ph - MARGIN).max(min_y); (target_x.clamp(min_x, max_x), target_y.clamp(min_y, max_y)) } @@ -73,12 +93,7 @@ fn calculate_anchored_position( open_above: bool, ) -> (i32, i32) { let (pw, ph) = physical_panel_size(panel_size, scale_factor); - // Tray icon widths are tens of pixels, far below i32::MAX. - #[expect( - clippy::cast_possible_wrap, - reason = "tray icon pixel dimensions fit in i32" - )] - let anchor_x = icon_rect.x + (icon_rect.width as i32) / 2; + let anchor_x = icon_rect.x + icon_rect.signed_width() / 2; let target_x = anchor_x - pw / 2; let target_y = if open_above { anchor_y - ph - GAP @@ -86,17 +101,7 @@ fn calculate_anchored_position( anchor_y + GAP }; - clamp_to_work_area(target_x, target_y, monitor_rect, panel_size, scale_factor) -} - -pub fn clamp_position_to_work_area( - target_x: i32, - target_y: i32, - monitor_rect: &Rect, - panel_size: &PanelSize, - scale_factor: f64, -) -> (i32, i32) { - clamp_to_work_area(target_x, target_y, monitor_rect, panel_size, scale_factor) + clamp_position_to_work_area(target_x, target_y, monitor_rect, panel_size, scale_factor) } /// Calculate panel position anchored to a tray icon rectangle. @@ -114,21 +119,16 @@ pub fn calculate_panel_position( scale_factor: f64, ) -> (i32, i32) { let my = work_area.y; - // Work-area and icon dimensions are physical pixels, bounded well below i32::MAX. - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let mh = work_area.height as i32; + let mh = work_area.signed_height(); - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let icon_cy = icon_rect.y + (icon_rect.height as i32) / 2; + let icon_cy = icon_rect.y + icon_rect.signed_height() / 2; let monitor_cy = my + mh / 2; let open_above = icon_cy > monitor_cy; - // Icon bounds are physical pixels, bounded well below i32::MAX. - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] let anchor_y = if open_above { icon_rect.y } else { - icon_rect.y + icon_rect.height as i32 + icon_rect.bottom() }; let position = calculate_anchored_position( @@ -139,18 +139,9 @@ pub fn calculate_panel_position( anchor_y, open_above, ); - // Monitor and work-area widths are physical pixels, bounded well below i32::MAX. - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let bounds_right = monitor_bounds.x + monitor_bounds.width as i32; - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let work_right = work_area.x + work_area.width as i32; - - if work_area.x > monitor_bounds.x || work_right < bounds_right { + if work_area.x > monitor_bounds.x || work_area.right() < monitor_bounds.right() { let (_, ph) = physical_panel_size(panel_size, scale_factor); - // Work-area height is a physical pixel count, bounded well below i32::MAX. - #[expect(clippy::cast_possible_wrap, reason = "pixel dimensions fit in i32")] - let work_height = work_area.height as i32; - (position.0, work_area.y + work_height - ph - MARGIN) + (position.0, work_area.bottom() - ph - MARGIN) } else { position } @@ -173,7 +164,7 @@ pub fn calculate_cursor_position( reason = "physical cursor coordinates fit i32" )] let y = (cursor.1.round() as i32).saturating_add(GAP); - clamp_to_work_area(x, y, work_area, panel_size, scale_factor) + clamp_position_to_work_area(x, y, work_area, panel_size, scale_factor) } /// Legacy shortcut placement: 22 % from left, vertically centred. @@ -185,17 +176,8 @@ pub fn calculate_shortcut_position( let (pw, ph) = physical_panel_size(panel_size, scale_factor); let mx = monitor_rect.x; let my = monitor_rect.y; - // Monitor dimensions are physical pixels, bounded well below i32::MAX. - #[expect( - clippy::cast_possible_wrap, - reason = "monitor pixel dimensions fit in i32" - )] - let mw = monitor_rect.width as i32; - #[expect( - clippy::cast_possible_wrap, - reason = "monitor pixel dimensions fit in i32" - )] - let mh = monitor_rect.height as i32; + let mw = monitor_rect.signed_width(); + let mh = monitor_rect.signed_height(); // Shortcut offset is truncated to a whole pixel by design. #[expect(clippy::cast_possible_truncation, reason = "whole units by design")] @@ -212,22 +194,8 @@ pub fn calculate_shortcut_position( mod tests { use super::*; - fn standard_monitor() -> Rect { - Rect { - x: 0, - y: 0, - width: 2400, - height: 1080, - } - } - fn hd_monitor() -> Rect { - Rect { - x: 0, - y: 0, - width: 1920, - height: 1080, - } + Rect::new(0, 0, 1920, 1080) } fn panel() -> PanelSize { @@ -241,12 +209,7 @@ mod tests { #[test] fn bottom_taskbar_panel_opens_above() { - let icon = Rect { - x: 1800, - y: 1040, - width: 24, - height: 24, - }; + let icon = Rect::new(1800, 1040, 24, 24); let monitor = hd_monitor(); let (_, y) = calculate_panel_position(&icon, &monitor, &monitor, &panel(), 1.0); assert!(y < icon.y, "panel should sit above the icon"); @@ -254,12 +217,7 @@ mod tests { #[test] fn top_taskbar_panel_opens_below() { - let icon = Rect { - x: 900, - y: 4, - width: 24, - height: 24, - }; + let icon = Rect::new(900, 4, 24, 24); let monitor = hd_monitor(); let (_, y) = calculate_panel_position(&icon, &monitor, &monitor, &panel(), 1.0); assert!( @@ -270,12 +228,7 @@ mod tests { #[test] fn horizontal_centre_on_icon() { - let icon = Rect { - x: 960, - y: 1040, - width: 24, - height: 24, - }; + let icon = Rect::new(960, 1040, 24, 24); let monitor = hd_monitor(); let (x, _) = calculate_panel_position(&icon, &monitor, &monitor, &panel(), 1.0); let icon_cx = icon.x + 12; @@ -289,12 +242,7 @@ mod tests { #[test] fn clamped_left_edge() { - let icon = Rect { - x: 0, - y: 1040, - width: 24, - height: 24, - }; + let icon = Rect::new(0, 1040, 24, 24); let monitor = hd_monitor(); let (x, _) = calculate_panel_position(&icon, &monitor, &monitor, &panel(), 1.0); assert!(x >= MARGIN, "panel must not exceed left margin"); @@ -302,12 +250,7 @@ mod tests { #[test] fn clamped_right_edge() { - let icon = Rect { - x: 1900, - y: 1040, - width: 24, - height: 24, - }; + let icon = Rect::new(1900, 1040, 24, 24); let monitor = hd_monitor(); let (x, _) = calculate_panel_position(&icon, &monitor, &monitor, &panel(), 1.0); assert!( @@ -318,12 +261,7 @@ mod tests { #[test] fn clamped_top_edge() { - let icon = Rect { - x: 960, - y: 4, - width: 24, - height: 24, - }; + let icon = Rect::new(960, 4, 24, 24); let monitor = hd_monitor(); let (_, y) = calculate_panel_position(&icon, &monitor, &monitor, &panel(), 1.0); assert!(y >= MARGIN, "panel must not exceed top margin"); @@ -331,18 +269,8 @@ mod tests { #[test] fn top_taskbar_work_area_clamps_open_below_to_min_y() { - let work_area = Rect { - x: 0, - y: 40, - width: 1920, - height: 1040, - }; - let icon = Rect { - x: 960, - y: 4, - width: 24, - height: 24, - }; + let work_area = Rect::new(0, 40, 1920, 1040); + let icon = Rect::new(960, 4, 24, 24); let monitor = hd_monitor(); let (_, y) = calculate_panel_position(&icon, &monitor, &work_area, &panel(), 1.0); assert_eq!(y, work_area.y + MARGIN); @@ -351,18 +279,8 @@ mod tests { #[test] fn left_taskbar_bottom_aligns_panel() { let monitor = hd_monitor(); - let work_area = Rect { - x: 40, - y: 0, - width: 1880, - height: 1080, - }; - let icon = Rect { - x: 8, - y: 1048, - width: 24, - height: 24, - }; + let work_area = Rect::new(40, 0, 1880, 1080); + let icon = Rect::new(8, 1048, 24, 24); let (_, y) = calculate_panel_position(&icon, &monitor, &work_area, &panel(), 1.0); @@ -371,24 +289,9 @@ mod tests { #[test] fn high_dpi_right_taskbar_bottom_aligns_panel() { - let monitor = Rect { - x: 0, - y: 0, - width: 3840, - height: 2160, - }; - let work_area = Rect { - x: 0, - y: 0, - width: 3760, - height: 2160, - }; - let icon = Rect { - x: 3808, - y: 2128, - width: 24, - height: 24, - }; + let monitor = Rect::new(0, 0, 3840, 2160); + let work_area = Rect::new(0, 0, 3760, 2160); + let icon = Rect::new(3808, 2128, 24, 24); let (_, y) = calculate_panel_position(&icon, &monitor, &work_area, &panel(), 2.0); @@ -397,18 +300,8 @@ mod tests { #[test] fn multi_monitor_offset() { - let monitor = Rect { - x: 1920, - y: 0, - width: 1920, - height: 1080, - }; - let icon = Rect { - x: 3700, - y: 1040, - width: 24, - height: 24, - }; + let monitor = Rect::new(1920, 0, 1920, 1080); + let icon = Rect::new(3700, 1040, 24, 24); let (x, _) = calculate_panel_position(&icon, &monitor, &monitor, &panel(), 1.0); assert!(x >= monitor.x + MARGIN); assert!( @@ -419,12 +312,7 @@ mod tests { #[test] fn high_dpi_positioning() { - let icon = Rect { - x: 960, - y: 1040, - width: 24, - height: 24, - }; + let icon = Rect::new(960, 1040, 24, 24); let monitor = hd_monitor(); let (x1, y1) = calculate_panel_position(&icon, &monitor, &monitor, &panel(), 1.0); let (x2, y2) = calculate_panel_position(&icon, &monitor, &monitor, &panel(), 2.0); @@ -460,12 +348,7 @@ mod tests { #[test] fn cursor_open_clamps_on_high_dpi_monitor_with_negative_origin() { - let work_area = Rect { - x: -2560, - y: 0, - width: 2560, - height: 1400, - }; + let work_area = Rect::new(-2560, 0, 2560, 1400); let panel = PanelSize { width: 400, height: 600, @@ -478,12 +361,7 @@ mod tests { #[test] fn oversized_cursor_panel_keeps_its_top_left_visible() { - let work_area = Rect { - x: 1920, - y: -100, - width: 500, - height: 400, - }; + let work_area = Rect::new(1920, -100, 500, 400); assert_eq!( calculate_cursor_position((2200.0, 250.0), &work_area, &panel(), 2.0), (1928, -92) @@ -509,12 +387,7 @@ mod tests { #[test] fn shortcut_clamped_small_monitor() { - let monitor = Rect { - x: 0, - y: 0, - width: 500, - height: 600, - }; + let monitor = Rect::new(0, 0, 500, 600); let (x, y) = calculate_shortcut_position(&monitor, &panel(), 1.0); assert!(x >= MARGIN); assert!( From 2645a8334d91a1e4d4fa6f46e5301a740e6cf48b Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:18:16 +0700 Subject: [PATCH 12/22] Build shell test monitors with one helper --- .../src-tauri/src/shell/tests.rs | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/shell/tests.rs b/apps/desktop-tauri/src-tauri/src/shell/tests.rs index f46b89bc20a..b7cc50c22b8 100644 --- a/apps/desktop-tauri/src-tauri/src/shell/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/shell/tests.rs @@ -17,6 +17,14 @@ use crate::surface::{SurfaceMode, SurfaceTransition}; use crate::surface_target::SurfaceTarget; use crate::window_positioner::{self, Rect}; +fn monitor(bounds: Rect, work_area: Rect, scale_factor: f64) -> MonitorPlacement { + MonitorPlacement { + bounds, + work_area, + scale_factor, + } +} + #[test] fn hide_to_tray_resets_hidden_target_to_summary() { let mut state = AppState::new(); @@ -271,11 +279,11 @@ fn preserved_visible_monitor_prefers_top_left_for_straddling_window() { #[test] fn inferred_tray_anchor_defaults_to_bottom_right_of_work_area() { - let monitor = MonitorPlacement { - bounds: Rect::new(0, 0, 1920, 1080), - work_area: Rect::new(0, 0, 1920, 1040), - scale_factor: 1.0, - }; + let monitor = monitor( + Rect::new(0, 0, 1920, 1080), + Rect::new(0, 0, 1920, 1040), + 1.0, + ); let anchor = inferred_tray_anchor_rect(&monitor); @@ -287,11 +295,11 @@ fn inferred_tray_anchor_defaults_to_bottom_right_of_work_area() { #[test] fn inferred_tray_anchor_supports_top_taskbar_layouts() { - let monitor = MonitorPlacement { - bounds: Rect::new(0, 0, 1920, 1080), - work_area: Rect::new(0, 40, 1920, 1040), - scale_factor: 1.0, - }; + let monitor = monitor( + Rect::new(0, 0, 1920, 1080), + Rect::new(0, 40, 1920, 1040), + 1.0, + ); let anchor = inferred_tray_anchor_rect(&monitor); @@ -301,11 +309,11 @@ fn inferred_tray_anchor_supports_top_taskbar_layouts() { #[test] fn inferred_tray_anchor_supports_left_taskbar_layouts() { - let monitor = MonitorPlacement { - bounds: Rect::new(0, 0, 1920, 1080), - work_area: Rect::new(40, 0, 1880, 1080), - scale_factor: 1.0, - }; + let monitor = monitor( + Rect::new(0, 0, 1920, 1080), + Rect::new(40, 0, 1880, 1080), + 1.0, + ); let anchor = inferred_tray_anchor_rect(&monitor); @@ -315,11 +323,11 @@ fn inferred_tray_anchor_supports_left_taskbar_layouts() { #[test] fn inferred_tray_anchor_supports_right_taskbar_layouts() { - let monitor = MonitorPlacement { - bounds: Rect::new(0, 0, 1920, 1080), - work_area: Rect::new(0, 0, 1880, 1080), - scale_factor: 1.0, - }; + let monitor = monitor( + Rect::new(0, 0, 1920, 1080), + Rect::new(0, 0, 1880, 1080), + 1.0, + ); let anchor = inferred_tray_anchor_rect(&monitor); @@ -329,11 +337,11 @@ fn inferred_tray_anchor_supports_right_taskbar_layouts() { #[test] fn inferred_tray_panel_position_uses_tray_style_corner_fallback() { - let monitor = MonitorPlacement { - bounds: Rect::new(0, 0, 1920, 1080), - work_area: Rect::new(0, 0, 1920, 1040), - scale_factor: 1.0, - }; + let monitor = monitor( + Rect::new(0, 0, 1920, 1080), + Rect::new(0, 0, 1920, 1040), + 1.0, + ); let position = inferred_tray_panel_position_for_monitor(&monitor); @@ -485,11 +493,7 @@ fn hidden_surface_snapshot_matches_non_visible_shell_state() { #[test] fn remembered_popout_position_clamps_using_stored_size() { - let monitor = MonitorPlacement { - bounds: Rect::new(0, 0, 1000, 800), - work_area: Rect::new(0, 0, 1000, 800), - scale_factor: 1.0, - }; + let monitor = monitor(Rect::new(0, 0, 1000, 800), Rect::new(0, 0, 1000, 800), 1.0); let stored = crate::geometry_store::StoredGeometry { x: 900, y: 700, From c23afc58640cb97164cc8b2ec8dcf538eb8f7b8c Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:20:31 +0700 Subject: [PATCH 13/22] Remove the uninvoked float bar commands and claude_accounts_list registration --- .../src-tauri/src/commands/claude_accounts.rs | 1 - .../src-tauri/src/floatbar/commands.rs | 75 +------------------ .../src-tauri/src/floatbar/mod.rs | 24 +----- .../src-tauri/src/floatbar/window.rs | 32 ++++---- apps/desktop-tauri/src-tauri/src/main.rs | 6 -- 5 files changed, 21 insertions(+), 117 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/claude_accounts.rs b/apps/desktop-tauri/src-tauri/src/commands/claude_accounts.rs index 8de0b21a223..a86606b96d8 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/claude_accounts.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/claude_accounts.rs @@ -15,7 +15,6 @@ use tauri::Manager; static MUTATION: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); const AMBIENT_RECONCILIATION_GRACE: Duration = Duration::from_secs(5); -#[tauri::command] pub fn claude_accounts_list() -> Result, String> { AccountManager::new() .and_then(|m| m.list()) diff --git a/apps/desktop-tauri/src-tauri/src/floatbar/commands.rs b/apps/desktop-tauri/src-tauri/src/floatbar/commands.rs index 887045c5ddc..5a9f5043ef7 100644 --- a/apps/desktop-tauri/src-tauri/src/floatbar/commands.rs +++ b/apps/desktop-tauri/src-tauri/src/floatbar/commands.rs @@ -1,66 +1,13 @@ //! Tauri commands that drive the floating-bar window. //! -//! These are thin wrappers around the local `window` module. User-initiated -//! changes keep persisted settings in sync, while the resize command applies a -//! new size and the native interaction state together. +//! The resize command applies a new size and the native interaction state +//! together. -use codexbar::settings::{Settings, clamp_float_bar_opacity, normalize_float_bar_orientation}; +use codexbar::settings::Settings; use tauri::{AppHandle, Manager}; use super::window as floatbar_window; -#[tauri::command] -pub async fn show_float_bar(app: AppHandle) -> Result<(), String> { - let mut settings = Settings::load(); - settings.float_bar_enabled = true; - settings.save().map_err(|e| e.to_string())?; - - floatbar_window::show( - &app, - settings.float_bar_opacity, - &settings.float_bar_orientation, - &settings.float_bar_style, - settings.float_bar_click_through, - ) -} - -#[tauri::command] -pub fn hide_float_bar(app: AppHandle) -> Result<(), String> { - let mut settings = Settings::load(); - settings.float_bar_enabled = false; - settings.save().map_err(|e| e.to_string())?; - floatbar_window::hide(&app) -} - -#[tauri::command] -pub fn set_float_bar_opacity(app: AppHandle, opacity: u8) -> Result<(), String> { - let opacity = clamp_float_bar_opacity(opacity); - let mut settings = Settings::load(); - settings.float_bar_opacity = opacity; - settings.save().map_err(|e| e.to_string())?; - - if let Some(window) = app.get_webview_window(floatbar_window::FLOATBAR_LABEL) { - floatbar_window::apply_no_activate(&window); - floatbar_window::apply_opacity(&window, opacity); - floatbar_window::apply_always_on_top(&window); - } - Ok(()) -} - -#[tauri::command] -pub fn set_float_bar_click_through(app: AppHandle, enabled: bool) -> Result<(), String> { - let mut settings = Settings::load(); - settings.float_bar_click_through = enabled; - settings.save().map_err(|e| e.to_string())?; - - if let Some(window) = app.get_webview_window(floatbar_window::FLOATBAR_LABEL) { - floatbar_window::apply_no_activate(&window); - floatbar_window::apply_click_through(&window, enabled); - floatbar_window::apply_always_on_top(&window); - } - Ok(()) -} - #[tauri::command] pub fn resize_float_bar(app: AppHandle, width: f64, height: f64) -> Result<(), String> { let settings = Settings::load(); @@ -71,19 +18,3 @@ pub fn resize_float_bar(app: AppHandle, width: f64, height: f64) -> Result<(), S } Ok(()) } - -#[tauri::command] -pub fn set_float_bar_orientation(app: AppHandle, orientation: String) -> Result<(), String> { - let orientation = normalize_float_bar_orientation(&orientation); - let mut settings = Settings::load(); - settings.float_bar_orientation = orientation; - settings.save().map_err(|e| e.to_string())?; - - // The webview re-reads orientation from settings on every config - // change — emit the live-update event so it re-lays-out without us - // needing to destroy/recreate the window (which races with Tauri's - // async window lifecycle and can crash on Windows). - use tauri::Emitter; - let _ = app.emit(super::FLOAT_BAR_CONFIG_CHANGED_EVENT, ()); - Ok(()) -} diff --git a/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs b/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs index 5e2c1f79086..22f8ce94a12 100644 --- a/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs +++ b/apps/desktop-tauri/src-tauri/src/floatbar/mod.rs @@ -28,13 +28,7 @@ pub fn install(app: &tauri::AppHandle) { if persisted.float_bar_enabled { // Best-effort floatbar show on startup; a show failure is logged // by the window layer and is non-fatal to app setup. - let _show = window::show( - app, - persisted.float_bar_opacity, - &persisted.float_bar_orientation, - &persisted.float_bar_style, - persisted.float_bar_click_through, - ); + let _show = window::show(app, &persisted); } } @@ -85,13 +79,7 @@ pub fn toggle(app: &tauri::AppHandle) { if settings.float_bar_enabled { // Best-effort show; window::show logs its own failures and the // toggle is fire-and-forget from the tray. - let _show = window::show( - app, - settings.float_bar_opacity, - &settings.float_bar_orientation, - &settings.float_bar_style, - settings.float_bar_click_through, - ); + let _show = window::show(app, &settings); } else { // Best-effort hide; a hide failure leaves the bar visible, which // the next toggle corrects — non-fatal. @@ -105,13 +93,7 @@ pub fn toggle(app: &tauri::AppHandle) { pub fn apply_state(app: &tauri::AppHandle, settings: &Settings) { let open = app.get_webview_window(FLOATBAR_LABEL).is_some(); if settings.float_bar_enabled && !open { - let _show = window::show( - app, - settings.float_bar_opacity, - &settings.float_bar_orientation, - &settings.float_bar_style, - settings.float_bar_click_through, - ); + let _show = window::show(app, settings); } else if !settings.float_bar_enabled && open { let _hide = window::hide(app); } else if let Some(w) = app.get_webview_window(FLOATBAR_LABEL) { diff --git a/apps/desktop-tauri/src-tauri/src/floatbar/window.rs b/apps/desktop-tauri/src-tauri/src/floatbar/window.rs index dacafb9a8d7..e1d28bc137d 100644 --- a/apps/desktop-tauri/src-tauri/src/floatbar/window.rs +++ b/apps/desktop-tauri/src-tauri/src/floatbar/window.rs @@ -230,24 +230,17 @@ pub fn opacity_to_alpha(opacity: u8) -> u8 { /// already open. The bar never takes focus. Position is restored from the /// geometry store keyed by `floatbar`; on first launch the window is /// centered horizontally near the top of the primary monitor. -pub fn show( - app: &tauri::AppHandle, - opacity: u8, - orientation: &str, - style: &str, - click_through: bool, -) -> Result<(), String> { +pub fn show(app: &tauri::AppHandle, settings: &codexbar::settings::Settings) -> Result<(), String> { + let opacity = settings.float_bar_opacity; + let orientation = settings.float_bar_orientation.as_str(); + let style = settings.float_bar_style.as_str(); + let click_through = settings.float_bar_click_through; if let Some(window) = app.get_webview_window(FLOATBAR_LABEL) { apply_opacity(&window, opacity); apply_click_through(&window, click_through); let _ = ensure_visible_on_active_monitor(&window, style); // Re-assert after possible unminimize/relocate so focus stays off. - apply_no_activate(&window); - apply_always_on_top(&window); - window.show().map_err(|e| e.to_string())?; - apply_always_on_top(&window); - super::topmost_guard::set_active(true); - return Ok(()); + return present(&window); } let (w, h) = initial_size(orientation); @@ -315,10 +308,15 @@ pub fn show( apply_opacity(&win, opacity); apply_click_through(&win, click_through); - apply_no_activate(&win); - apply_always_on_top(&win); - win.show().map_err(|e| e.to_string())?; - apply_always_on_top(&win); + present(&win) +} + +/// Show the bar without activating it and keep it above the taskbar. +fn present(window: &tauri::WebviewWindow) -> Result<(), String> { + apply_no_activate(window); + apply_always_on_top(window); + window.show().map_err(|e| e.to_string())?; + apply_always_on_top(window); super::topmost_guard::set_active(true); Ok(()) } diff --git a/apps/desktop-tauri/src-tauri/src/main.rs b/apps/desktop-tauri/src-tauri/src/main.rs index f63f9a9f4a2..1aac39814a8 100644 --- a/apps/desktop-tauri/src-tauri/src/main.rs +++ b/apps/desktop-tauri/src-tauri/src/main.rs @@ -221,7 +221,6 @@ fn main() { commands::get_cached_providers, commands::get_deepseek_pricing_status, commands::codex_accounts_list, - commands::claude_accounts_list, commands::get_claude_accounts_state, commands::claude_reconciliation_state, commands::claude_account_add, @@ -318,12 +317,7 @@ fn main() { commands::set_ui_language, commands::open_path, tray_visibility::tray_visibility_status, - floatbar::show_float_bar, - floatbar::hide_float_bar, - floatbar::set_float_bar_opacity, - floatbar::set_float_bar_click_through, floatbar::resize_float_bar, - floatbar::set_float_bar_orientation, ]) .setup(move |app| { if let Err(error) = codexbar::providers::claude::accounts::cleanup_abandoned_logins() { From c43b6c6ed08b82abf34cb8fced1cc873755fa41f Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:22:20 +0700 Subject: [PATCH 14/22] Move inline shell test modules into their own files --- .../src-tauri/src/commands/bridge.rs | 118 +-- .../src-tauri/src/commands/bridge/tests.rs | 114 +++ .../src-tauri/src/commands/providers.rs | 96 +-- .../providers/predictive_warning_tests.rs | 93 +++ .../src-tauri/src/commands/settings.rs | 419 +--------- .../src-tauri/src/commands/settings/tests.rs | 416 ++++++++++ .../src-tauri/src/commands/usage_spend.rs | 187 +---- .../commands/usage_spend/cache_key_tests.rs | 184 +++++ .../src-tauri/src/floatbar/window.rs | 176 +--- .../src-tauri/src/floatbar/window/tests.rs | 173 ++++ .../src-tauri/src/usage_metric.rs | 753 +----------------- .../src-tauri/src/usage_metric/tests.rs | 739 +++++++++++++++++ 12 files changed, 1725 insertions(+), 1743 deletions(-) create mode 100644 apps/desktop-tauri/src-tauri/src/commands/bridge/tests.rs create mode 100644 apps/desktop-tauri/src-tauri/src/commands/providers/predictive_warning_tests.rs create mode 100644 apps/desktop-tauri/src-tauri/src/commands/settings/tests.rs create mode 100644 apps/desktop-tauri/src-tauri/src/commands/usage_spend/cache_key_tests.rs create mode 100644 apps/desktop-tauri/src-tauri/src/floatbar/window/tests.rs create mode 100644 apps/desktop-tauri/src-tauri/src/usage_metric/tests.rs diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 1b0f18d93ed..005c742befd 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -1126,120 +1126,4 @@ pub(super) fn parse_metric_preference(s: &str) -> Option { } #[cfg(test)] -mod tests { - use super::*; - - fn snapshot_window_with( - used_percent: f64, - window_minutes: Option, - resets_at: Option>, - reset_description: Option, - ) -> RateWindowSnapshot { - RateWindowSnapshot { - used_percent, - remaining_percent: 100.0 - used_percent, - window_minutes, - resets_at: resets_at.map(|dt| dt.to_rfc3339()), - reset_description, - ..Default::default() - } - } - - #[test] - fn tray_status_prefers_relative_reset_countdown() { - let window = snapshot_window_with( - 13.0, - Some(300), - Some(chrono::Utc::now() + chrono::Duration::minutes(125)), - Some("Jun 10 at 3:00PM".to_string()), - ); - - let label = compact_tray_status_label(&window, Language::English); - - assert!(label.starts_with("13% • Resets in 2h ")); - assert!(label.ends_with('m')); - assert!(!label.contains("Jun 10")); - } - - #[test] - fn tray_status_normalizes_fallback_reset_description() { - let window = snapshot_window_with(8.0, Some(300), None, Some("2h 05m".to_string())); - - assert_eq!( - compact_tray_status_label(&window, Language::English), - "8% • Resets in 2h 05m" - ); - } - - #[test] - fn credit_balance_detail_crosses_the_bridge_and_is_not_a_reset_phrase() { - let rw = RateWindow::with_details(25.0, None, None, Some("750 / 1000 credits left".into())) - .with_description_as_detail(); - let window = RateWindowSnapshot::from_rate_window(&rw); - - assert!(window.description_is_detail); - assert_eq!( - window.reset_description.as_deref(), - Some("750 / 1000 credits left") - ); - let json = serde_json::to_value(&window).unwrap(); - assert_eq!(json["descriptionIsDetail"], true); - assert_eq!(compact_tray_status_label(&window, Language::English), "25%"); - - let plain = RateWindowSnapshot::from_rate_window(&RateWindow::new(10.0)); - let json = serde_json::to_value(&plain).unwrap(); - assert_eq!(json["descriptionIsDetail"], false); - } - - #[test] - fn japanese_tray_status_label_has_no_english_reset_text() { - use codexbar::settings::Language; - - let window = snapshot_window_with( - 13.0, - Some(300), - Some(chrono::Utc::now() + chrono::Duration::minutes(125)), - None, - ); - - let label = compact_tray_status_label(&window, Language::Japanese); - - assert!(label.contains("リセットまで"), "{label}"); - assert!(!label.to_ascii_lowercase().contains("resets in"), "{label}"); - assert!(label.contains("13%"), "{label}"); - } - - #[test] - fn japanese_tray_status_strips_english_fallback_reset_prefix() { - use codexbar::settings::Language; - - let window = - snapshot_window_with(8.0, Some(300), None, Some("Resets in 2h 05m".to_string())); - - let label = compact_tray_status_label(&window, Language::Japanese); - - assert!(label.contains("リセットまで"), "{label}"); - assert!(!label.to_ascii_lowercase().contains("resets in"), "{label}"); - assert!(label.contains("2時間 05分"), "{label}"); - } - - #[test] - fn tray_status_label_relocalizes_without_refetch() { - let window = snapshot_window_with( - 13.0, - Some(300), - Some(chrono::Utc::now() + chrono::Duration::minutes(125)), - None, - ); - - let english = compact_tray_status_label(&window, Language::English); - let japanese = compact_tray_status_label(&window, Language::Japanese); - - assert!(english.contains("Resets in"), "{english}"); - assert!(japanese.contains("リセットまで"), "{japanese}"); - assert!( - !japanese.to_ascii_lowercase().contains("resets in"), - "{japanese}" - ); - } -} +mod tests; diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge/tests.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge/tests.rs new file mode 100644 index 00000000000..85977aec7f8 --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge/tests.rs @@ -0,0 +1,114 @@ +use super::*; + +fn snapshot_window_with( + used_percent: f64, + window_minutes: Option, + resets_at: Option>, + reset_description: Option, +) -> RateWindowSnapshot { + RateWindowSnapshot { + used_percent, + remaining_percent: 100.0 - used_percent, + window_minutes, + resets_at: resets_at.map(|dt| dt.to_rfc3339()), + reset_description, + ..Default::default() + } +} + +#[test] +fn tray_status_prefers_relative_reset_countdown() { + let window = snapshot_window_with( + 13.0, + Some(300), + Some(chrono::Utc::now() + chrono::Duration::minutes(125)), + Some("Jun 10 at 3:00PM".to_string()), + ); + + let label = compact_tray_status_label(&window, Language::English); + + assert!(label.starts_with("13% • Resets in 2h ")); + assert!(label.ends_with('m')); + assert!(!label.contains("Jun 10")); +} + +#[test] +fn tray_status_normalizes_fallback_reset_description() { + let window = snapshot_window_with(8.0, Some(300), None, Some("2h 05m".to_string())); + + assert_eq!( + compact_tray_status_label(&window, Language::English), + "8% • Resets in 2h 05m" + ); +} + +#[test] +fn credit_balance_detail_crosses_the_bridge_and_is_not_a_reset_phrase() { + let rw = RateWindow::with_details(25.0, None, None, Some("750 / 1000 credits left".into())) + .with_description_as_detail(); + let window = RateWindowSnapshot::from_rate_window(&rw); + + assert!(window.description_is_detail); + assert_eq!( + window.reset_description.as_deref(), + Some("750 / 1000 credits left") + ); + let json = serde_json::to_value(&window).unwrap(); + assert_eq!(json["descriptionIsDetail"], true); + assert_eq!(compact_tray_status_label(&window, Language::English), "25%"); + + let plain = RateWindowSnapshot::from_rate_window(&RateWindow::new(10.0)); + let json = serde_json::to_value(&plain).unwrap(); + assert_eq!(json["descriptionIsDetail"], false); +} + +#[test] +fn japanese_tray_status_label_has_no_english_reset_text() { + use codexbar::settings::Language; + + let window = snapshot_window_with( + 13.0, + Some(300), + Some(chrono::Utc::now() + chrono::Duration::minutes(125)), + None, + ); + + let label = compact_tray_status_label(&window, Language::Japanese); + + assert!(label.contains("リセットまで"), "{label}"); + assert!(!label.to_ascii_lowercase().contains("resets in"), "{label}"); + assert!(label.contains("13%"), "{label}"); +} + +#[test] +fn japanese_tray_status_strips_english_fallback_reset_prefix() { + use codexbar::settings::Language; + + let window = snapshot_window_with(8.0, Some(300), None, Some("Resets in 2h 05m".to_string())); + + let label = compact_tray_status_label(&window, Language::Japanese); + + assert!(label.contains("リセットまで"), "{label}"); + assert!(!label.to_ascii_lowercase().contains("resets in"), "{label}"); + assert!(label.contains("2時間 05分"), "{label}"); +} + +#[test] +fn tray_status_label_relocalizes_without_refetch() { + let window = snapshot_window_with( + 13.0, + Some(300), + Some(chrono::Utc::now() + chrono::Duration::minutes(125)), + None, + ); + + let english = compact_tray_status_label(&window, Language::English); + let japanese = compact_tray_status_label(&window, Language::Japanese); + + assert!(english.contains("Resets in"), "{english}"); + assert!(japanese.contains("リセットまで"), "{japanese}"); + assert!( + !japanese.to_ascii_lowercase().contains("resets in"), + "{japanese}" + ); +} diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers.rs b/apps/desktop-tauri/src-tauri/src/commands/providers.rs index 9906e5048d7..97acacca875 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers.rs @@ -1302,98 +1302,4 @@ pub fn get_cached_providers( } #[cfg(test)] -mod predictive_warning_tests { - use super::*; - - fn empty_snapshot() -> ProviderUsageSnapshot { - let metadata = codexbar::core::instantiate_provider(ProviderId::Claude) - .metadata() - .clone(); - ProviderUsageSnapshot::from_error( - ProviderId::Claude, - &metadata, - "unused".to_string(), - codexbar::core::ProviderStateKind::Unknown, - ) - } - - #[test] - fn quota_notification_account_identity_prefers_token_then_email() { - let account_id = uuid::Uuid::parse_str("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa").unwrap(); - let mut snapshot = empty_snapshot(); - snapshot.account_email = Some("Person@Example.com".to_string()); - snapshot.account_organization = Some("Acme Org".to_string()); - snapshot.plan_name = Some("Pro".to_string()); - - assert_eq!( - quota_notification_account_identity(&snapshot, Some(account_id)), - "token-account:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" - ); - assert_eq!( - quota_notification_account_identity(&snapshot, None), - "person@example.com" - ); - - snapshot.account_email = None; - assert_eq!( - quota_notification_account_identity(&snapshot, None), - "org:acme org" - ); - - snapshot.account_organization = None; - snapshot.source_label = "oauth".to_string(); - assert_eq!( - quota_notification_account_identity(&snapshot, None), - "claude:oauth:unknown" - ); - - snapshot.plan_name = None; - snapshot.source_label = "cli (reduced fidelity)".to_string(); - assert_eq!( - quota_notification_account_identity(&snapshot, None), - "claude:cli:unknown" - ); - } - - /// The forecast scope key and the notification identity must never disagree. - /// If they did, one account would be seen as two identities and its burn history - /// would be split, silently halving the sample count behind every forecast. - #[test] - fn forecast_account_key_matches_notification_identity() { - use crate::commands::bridge::forecast_account_key; - - let token = uuid::Uuid::parse_str("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa").unwrap(); - let mut usage = codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(1.0)); - let mut snapshot = empty_snapshot(); - - for (email, org) in [ - (Some("Person@Example.com"), Some("Acme Org")), - (Some("Person@Example.com"), None), - (None, Some("Acme Org")), - (None, None), - ] { - usage.account_email = email.map(str::to_string); - usage.account_organization = org.map(str::to_string); - snapshot.account_email = usage.account_email.clone(); - snapshot.account_organization = usage.account_organization.clone(); - - for tok in [Some(token), None] { - assert_eq!( - forecast_account_key(&usage, tok).unwrap_or_default(), - quota_notification_account_identity(&snapshot, tok), - "identity drift for email={email:?} org={org:?} token={tok:?}" - ); - } - } - } - - #[test] - fn provider_timeout_error_message_is_plain_timeout() { - // The elapsed-timeout path reuses the provider-error path, which - // redacts the message; the user must still see "Timeout". - assert_eq!( - codexbar::logging::safe_error_message(codexbar::core::ProviderError::Timeout), - "Timeout" - ); - } -} +mod predictive_warning_tests; diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers/predictive_warning_tests.rs b/apps/desktop-tauri/src-tauri/src/commands/providers/predictive_warning_tests.rs new file mode 100644 index 00000000000..5afb8077183 --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/providers/predictive_warning_tests.rs @@ -0,0 +1,93 @@ +use super::*; + +fn empty_snapshot() -> ProviderUsageSnapshot { + let metadata = codexbar::core::instantiate_provider(ProviderId::Claude) + .metadata() + .clone(); + ProviderUsageSnapshot::from_error( + ProviderId::Claude, + &metadata, + "unused".to_string(), + codexbar::core::ProviderStateKind::Unknown, + ) +} + +#[test] +fn quota_notification_account_identity_prefers_token_then_email() { + let account_id = uuid::Uuid::parse_str("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa").unwrap(); + let mut snapshot = empty_snapshot(); + snapshot.account_email = Some("Person@Example.com".to_string()); + snapshot.account_organization = Some("Acme Org".to_string()); + snapshot.plan_name = Some("Pro".to_string()); + + assert_eq!( + quota_notification_account_identity(&snapshot, Some(account_id)), + "token-account:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" + ); + assert_eq!( + quota_notification_account_identity(&snapshot, None), + "person@example.com" + ); + + snapshot.account_email = None; + assert_eq!( + quota_notification_account_identity(&snapshot, None), + "org:acme org" + ); + + snapshot.account_organization = None; + snapshot.source_label = "oauth".to_string(); + assert_eq!( + quota_notification_account_identity(&snapshot, None), + "claude:oauth:unknown" + ); + + snapshot.plan_name = None; + snapshot.source_label = "cli (reduced fidelity)".to_string(); + assert_eq!( + quota_notification_account_identity(&snapshot, None), + "claude:cli:unknown" + ); +} + +/// The forecast scope key and the notification identity must never disagree. +/// If they did, one account would be seen as two identities and its burn history +/// would be split, silently halving the sample count behind every forecast. +#[test] +fn forecast_account_key_matches_notification_identity() { + use crate::commands::bridge::forecast_account_key; + + let token = uuid::Uuid::parse_str("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa").unwrap(); + let mut usage = codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(1.0)); + let mut snapshot = empty_snapshot(); + + for (email, org) in [ + (Some("Person@Example.com"), Some("Acme Org")), + (Some("Person@Example.com"), None), + (None, Some("Acme Org")), + (None, None), + ] { + usage.account_email = email.map(str::to_string); + usage.account_organization = org.map(str::to_string); + snapshot.account_email = usage.account_email.clone(); + snapshot.account_organization = usage.account_organization.clone(); + + for tok in [Some(token), None] { + assert_eq!( + forecast_account_key(&usage, tok).unwrap_or_default(), + quota_notification_account_identity(&snapshot, tok), + "identity drift for email={email:?} org={org:?} token={tok:?}" + ); + } + } +} + +#[test] +fn provider_timeout_error_message_is_plain_timeout() { + // The elapsed-timeout path reuses the provider-error path, which + // redacts the message; the user must still see "Timeout". + assert_eq!( + codexbar::logging::safe_error_message(codexbar::core::ProviderError::Timeout), + "Timeout" + ); +} diff --git a/apps/desktop-tauri/src-tauri/src/commands/settings.rs b/apps/desktop-tauri/src-tauri/src/commands/settings.rs index 42cb5b2d405..5324efca6ae 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/settings.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/settings.rs @@ -644,421 +644,4 @@ pub async fn update_settings( } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn preferred_currency_patch_accepts_supported_codes_and_rejects_unknown_codes() { - let mut settings = Settings::default(); - SettingsUpdate { - preferred_currency_code: Some("try".to_string()), - ..SettingsUpdate::default() - } - .apply_to(&mut settings) - .expect("TRY is supported"); - assert_eq!(settings.preferred_currency_code, "TRY"); - - let result = SettingsUpdate { - preferred_currency_code: Some("BTC".to_string()), - ..SettingsUpdate::default() - } - .apply_to(&mut settings); - assert!(matches!(result, Err(error) if error.contains("Unsupported preferred currency"))); - } - - #[test] - fn only_data_affecting_settings_refresh_providers() { - assert!( - SettingsUpdate { - enabled_providers: Some(vec!["codex".to_string()]), - ..Default::default() - } - .refreshes_provider_data() - ); - assert!( - SettingsUpdate { - claude_allow_reading_claude_code_credentials: Some(true), - ..Default::default() - } - .refreshes_provider_data() - ); - assert!( - !SettingsUpdate { - provider_metrics: Some(Default::default()), - tray_icon_mode: Some("single".to_string()), - ..Default::default() - } - .refreshes_provider_data() - ); - assert!( - !SettingsUpdate { - provider_hidden_usage_item_ids: Some( - [("codex".to_string(), Vec::new())].into_iter().collect(), - ), - ..Default::default() - } - .refreshes_provider_data() - ); - assert!( - !SettingsUpdate { - claude_daily_routines_usage_visible: Some(false), - ..Default::default() - } - .refreshes_provider_data() - ); - } - - #[test] - fn apply_advanced_settings_sets_claude_code_credentials_consent() { - let mut settings = Settings::default(); - assert!(!settings.claude_allow_reading_claude_code_credentials); - - SettingsUpdate { - claude_allow_reading_claude_code_credentials: Some(true), - ..Default::default() - } - .apply_advanced_settings(&mut settings); - assert!(settings.claude_allow_reading_claude_code_credentials); - - SettingsUpdate { - claude_allow_reading_claude_code_credentials: Some(false), - ..Default::default() - } - .apply_advanced_settings(&mut settings); - assert!(!settings.claude_allow_reading_claude_code_credentials); - } - - #[test] - fn apply_provider_usage_item_visibility_persists() { - let mut settings = Settings::default(); - SettingsUpdate { - provider_hidden_usage_item_ids: Some( - [("codex".to_string(), vec!["metric:secondary".to_string()])] - .into_iter() - .collect(), - ), - ..Default::default() - } - .apply_provider_settings(&mut settings); - - assert_eq!( - settings.hidden_usage_item_ids(ProviderId::Codex), - vec!["metric:secondary".to_string()] - ); - assert!(settings.codex_spark_usage_visible()); - } - - #[test] - fn copilot_seat_credit_update_distinguishes_missing_clear_and_value() { - let missing: SettingsUpdate = serde_json::from_str("{}").unwrap(); - assert_eq!(missing.copilot_seat_credit_entitlement, None); - - let clear: SettingsUpdate = - serde_json::from_str(r#"{"copilotSeatCreditEntitlement":null}"#).unwrap(); - assert_eq!(clear.copilot_seat_credit_entitlement, Some(None)); - - let value: SettingsUpdate = - serde_json::from_str(r#"{"copilotSeatCreditEntitlement":300}"#).unwrap(); - assert_eq!(value.copilot_seat_credit_entitlement, Some(Some(300.0))); - } - - #[test] - fn invalid_copilot_seat_credit_update_is_rejected_by_the_settings_setter() { - let mut settings = Settings::default(); - - let update: SettingsUpdate = - serde_json::from_str(r#"{"copilotSeatCreditEntitlement":-5}"#).unwrap(); - let error = update - .apply_to(&mut settings) - .expect_err("invalid allowance must be rejected"); - assert_eq!( - error, - "Copilot seat AI-credit allowance must be a finite number greater than zero" - ); - assert_eq!(settings.seat_credit_entitlement(ProviderId::Copilot), None); - } - - #[test] - fn cost_reporting_period_update_is_validated_and_applied() { - use codexbar::cost_reporting_period::CostReportingPeriod; - - let mut settings = Settings::default(); - let bad: SettingsUpdate = - serde_json::from_str(r#"{"costReportingPeriod":"rolling:0"}"#).unwrap(); - let error = bad - .apply_to(&mut settings) - .expect_err("zero-day window must be rejected"); - assert_eq!(error, "Invalid cost reporting period: rolling:0"); - - let good: SettingsUpdate = - serde_json::from_str(r#"{"costReportingPeriod":"month-to-date"}"#).unwrap(); - good.apply_to(&mut settings).expect("valid period applies"); - assert_eq!( - settings.cost_reporting_period, - CostReportingPeriod::MonthToDate - ); - } - - #[test] - fn display_settings_that_affect_tray_trigger_presentation_refresh() { - assert!( - SettingsUpdate { - switcher_shows_icons: Some(false), - ..Default::default() - } - .refreshes_tray_presentation() - ); - assert!( - SettingsUpdate { - reset_time_relative: Some(false), - ..Default::default() - } - .refreshes_tray_presentation() - ); - assert!( - SettingsUpdate { - menu_bar_color_pace: Some(true), - ..Default::default() - } - .refreshes_tray_presentation() - ); - assert!( - SettingsUpdate { - stacked_tray_top_provider: Some("claude".to_string()), - ..Default::default() - } - .refreshes_tray_presentation() - ); - } - - #[test] - fn apply_display_settings_updates_tray_pace_color() { - let mut settings = Settings::default(); - assert!(!settings.menu_bar_color_pace); - - SettingsUpdate { - menu_bar_color_pace: Some(true), - ..Default::default() - } - .apply_display_settings(&mut settings); - assert!(settings.menu_bar_color_pace); - - SettingsUpdate { - menu_bar_color_pace: Some(false), - ..Default::default() - } - .apply_display_settings(&mut settings); - assert!(!settings.menu_bar_color_pace); - } - - #[test] - fn stacked_tray_update_accepts_mode_and_clears_automatic_provider() { - let mut settings = Settings { - stacked_tray_top_provider: Some("codex".to_string()), - ..Settings::default() - }; - - SettingsUpdate { - tray_icon_mode: Some("stacked".to_string()), - stacked_tray_top_provider: Some(String::new()), - stacked_tray_bottom_provider: Some("claude".to_string()), - ..Default::default() - } - .apply_provider_settings(&mut settings); - - assert_eq!(settings.tray_icon_mode, TrayIconMode::Stacked); - assert_eq!(settings.stacked_tray_top_provider, None); - assert_eq!( - settings.stacked_tray_bottom_provider.as_deref(), - Some("claude") - ); - } - - #[test] - fn ui_language_change_refreshes_tray_presentation() { - assert!( - SettingsUpdate { - ui_language: Some("japanese".to_string()), - ..Default::default() - } - .refreshes_tray_presentation() - ); - } - - #[test] - fn apply_display_settings_clamps_window_scale_percent() { - let mut settings = Settings::default(); - - SettingsUpdate { - window_scale_percent: Some(300), - ..Default::default() - } - .apply_display_settings(&mut settings); - assert_eq!(settings.window_scale_percent, 250); - - SettingsUpdate { - window_scale_percent: Some(50), - ..Default::default() - } - .apply_display_settings(&mut settings); - assert_eq!(settings.window_scale_percent, 100); - } - - #[test] - fn apply_display_settings_clamps_tray_scale_percent() { - let mut settings = Settings::default(); - - SettingsUpdate { - tray_scale_percent: Some(300), - ..Default::default() - } - .apply_display_settings(&mut settings); - assert_eq!(settings.tray_scale_percent, 200); - - SettingsUpdate { - tray_scale_percent: Some(50), - ..Default::default() - } - .apply_display_settings(&mut settings); - assert_eq!(settings.tray_scale_percent, 100); - } - - #[test] - fn apply_display_settings_updates_tray_panel_always_on_top() { - let mut settings = Settings::default(); - assert!(!settings.tray_panel_always_on_top); - - SettingsUpdate { - tray_panel_always_on_top: Some(true), - ..Default::default() - } - .apply_display_settings(&mut settings); - assert!(settings.tray_panel_always_on_top); - - SettingsUpdate { - tray_panel_always_on_top: Some(false), - ..Default::default() - } - .apply_display_settings(&mut settings); - assert!(!settings.tray_panel_always_on_top); - } - - #[test] - fn apply_notification_settings_updates_sound() { - let mut settings = Settings::default(); - - SettingsUpdate { - notification_sound_theme: Some(codexbar::settings::NotificationSoundTheme::CodexBar), - ..Default::default() - } - .apply_notification_settings(&mut settings) - .expect("apply sound theme"); - - assert_eq!( - settings.notification_sound_theme, - codexbar::settings::NotificationSoundTheme::CodexBar - ); - } - - #[test] - fn apply_notification_settings_rejects_invalid_custom_sound() { - let mut settings = Settings::default(); - let result = SettingsUpdate { - notification_sound_paths: Some(codexbar::settings::NotificationSoundPaths { - high_usage: Some("relative.wav".to_string()), - ..Default::default() - }), - ..Default::default() - } - .apply_notification_settings(&mut settings); - - assert!(result.is_err()); - assert_eq!( - settings.notification_sound_paths, - codexbar::settings::NotificationSoundPaths::default() - ); - } - - fn switcher_patch(json: &str) -> SettingsUpdate { - serde_json::from_str(&format!(r#"{{"switcherShortcuts":{json}}}"#)).unwrap() - } - - #[test] - fn switcher_shortcuts_patch_stores_normalized_non_default_overrides() { - let mut settings = Settings::default(); - switcher_patch(r#"{"select2":"Alt+Cmd+2","previous":"left","next":"none"}"#) - .apply_to(&mut settings) - .expect("valid overrides are stored"); - - assert_eq!( - settings.switcher_shortcuts, - std::collections::BTreeMap::from([ - ("select2".to_string(), "ctrl+alt+2".to_string()), - ("next".to_string(), "none".to_string()), - ]) - ); - } - - #[test] - fn switcher_shortcuts_empty_patch_restores_defaults() { - let mut settings = Settings::default(); - switcher_patch(r#"{"next":"shift+right"}"#) - .apply_to(&mut settings) - .unwrap(); - switcher_patch("{}").apply_to(&mut settings).unwrap(); - - assert!(settings.switcher_shortcuts.is_empty()); - } - - #[test] - fn switcher_shortcuts_patch_rejects_invalid_maps_and_keeps_stored_value() { - let mut settings = Settings::default(); - switcher_patch(r#"{"next":"shift+right"}"#) - .apply_to(&mut settings) - .unwrap(); - - for (json, message) in [ - ( - r#"{"bogus":"ctrl+1"}"#, - "Unknown switcher shortcut action: bogus", - ), - ( - r#"{"next":"left"}"#, - "Each switcher shortcut can be assigned to only one action", - ), - ( - r#"{"next":"ctrl+r"}"#, - "ctrl+r is reserved and cannot be used as a switcher shortcut", - ), - (r#"{"next":"f1"}"#, "f1 is not a valid switcher shortcut"), - ] { - let error = switcher_patch(json) - .apply_to(&mut settings) - .expect_err(json); - assert_eq!(error, message); - } - assert_eq!( - settings.switcher_shortcuts.get("next").map(String::as_str), - Some("shift+right") - ); - } - - #[test] - fn switcher_shortcuts_snapshot_exposes_the_fully_resolved_map() { - let settings = Settings { - switcher_shortcuts: std::collections::BTreeMap::from([( - "next".to_string(), - "none".to_string(), - )]), - ..Settings::default() - }; - let value = - serde_json::to_value(super::super::bridge::SettingsSnapshot::from(settings)).unwrap(); - let map = &value["switcherShortcuts"]; - - assert_eq!(map["next"], "none"); - assert_eq!(map["previous"], "left"); - assert_eq!(map["select9"], "ctrl+9"); - assert_eq!(map.as_object().unwrap().len(), 11); - } -} +mod tests; diff --git a/apps/desktop-tauri/src-tauri/src/commands/settings/tests.rs b/apps/desktop-tauri/src-tauri/src/commands/settings/tests.rs new file mode 100644 index 00000000000..87ec170848d --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/settings/tests.rs @@ -0,0 +1,416 @@ +use super::*; + +#[test] +fn preferred_currency_patch_accepts_supported_codes_and_rejects_unknown_codes() { + let mut settings = Settings::default(); + SettingsUpdate { + preferred_currency_code: Some("try".to_string()), + ..SettingsUpdate::default() + } + .apply_to(&mut settings) + .expect("TRY is supported"); + assert_eq!(settings.preferred_currency_code, "TRY"); + + let result = SettingsUpdate { + preferred_currency_code: Some("BTC".to_string()), + ..SettingsUpdate::default() + } + .apply_to(&mut settings); + assert!(matches!(result, Err(error) if error.contains("Unsupported preferred currency"))); +} + +#[test] +fn only_data_affecting_settings_refresh_providers() { + assert!( + SettingsUpdate { + enabled_providers: Some(vec!["codex".to_string()]), + ..Default::default() + } + .refreshes_provider_data() + ); + assert!( + SettingsUpdate { + claude_allow_reading_claude_code_credentials: Some(true), + ..Default::default() + } + .refreshes_provider_data() + ); + assert!( + !SettingsUpdate { + provider_metrics: Some(Default::default()), + tray_icon_mode: Some("single".to_string()), + ..Default::default() + } + .refreshes_provider_data() + ); + assert!( + !SettingsUpdate { + provider_hidden_usage_item_ids: Some( + [("codex".to_string(), Vec::new())].into_iter().collect(), + ), + ..Default::default() + } + .refreshes_provider_data() + ); + assert!( + !SettingsUpdate { + claude_daily_routines_usage_visible: Some(false), + ..Default::default() + } + .refreshes_provider_data() + ); +} + +#[test] +fn apply_advanced_settings_sets_claude_code_credentials_consent() { + let mut settings = Settings::default(); + assert!(!settings.claude_allow_reading_claude_code_credentials); + + SettingsUpdate { + claude_allow_reading_claude_code_credentials: Some(true), + ..Default::default() + } + .apply_advanced_settings(&mut settings); + assert!(settings.claude_allow_reading_claude_code_credentials); + + SettingsUpdate { + claude_allow_reading_claude_code_credentials: Some(false), + ..Default::default() + } + .apply_advanced_settings(&mut settings); + assert!(!settings.claude_allow_reading_claude_code_credentials); +} + +#[test] +fn apply_provider_usage_item_visibility_persists() { + let mut settings = Settings::default(); + SettingsUpdate { + provider_hidden_usage_item_ids: Some( + [("codex".to_string(), vec!["metric:secondary".to_string()])] + .into_iter() + .collect(), + ), + ..Default::default() + } + .apply_provider_settings(&mut settings); + + assert_eq!( + settings.hidden_usage_item_ids(ProviderId::Codex), + vec!["metric:secondary".to_string()] + ); + assert!(settings.codex_spark_usage_visible()); +} + +#[test] +fn copilot_seat_credit_update_distinguishes_missing_clear_and_value() { + let missing: SettingsUpdate = serde_json::from_str("{}").unwrap(); + assert_eq!(missing.copilot_seat_credit_entitlement, None); + + let clear: SettingsUpdate = + serde_json::from_str(r#"{"copilotSeatCreditEntitlement":null}"#).unwrap(); + assert_eq!(clear.copilot_seat_credit_entitlement, Some(None)); + + let value: SettingsUpdate = + serde_json::from_str(r#"{"copilotSeatCreditEntitlement":300}"#).unwrap(); + assert_eq!(value.copilot_seat_credit_entitlement, Some(Some(300.0))); +} + +#[test] +fn invalid_copilot_seat_credit_update_is_rejected_by_the_settings_setter() { + let mut settings = Settings::default(); + + let update: SettingsUpdate = + serde_json::from_str(r#"{"copilotSeatCreditEntitlement":-5}"#).unwrap(); + let error = update + .apply_to(&mut settings) + .expect_err("invalid allowance must be rejected"); + assert_eq!( + error, + "Copilot seat AI-credit allowance must be a finite number greater than zero" + ); + assert_eq!(settings.seat_credit_entitlement(ProviderId::Copilot), None); +} + +#[test] +fn cost_reporting_period_update_is_validated_and_applied() { + use codexbar::cost_reporting_period::CostReportingPeriod; + + let mut settings = Settings::default(); + let bad: SettingsUpdate = + serde_json::from_str(r#"{"costReportingPeriod":"rolling:0"}"#).unwrap(); + let error = bad + .apply_to(&mut settings) + .expect_err("zero-day window must be rejected"); + assert_eq!(error, "Invalid cost reporting period: rolling:0"); + + let good: SettingsUpdate = + serde_json::from_str(r#"{"costReportingPeriod":"month-to-date"}"#).unwrap(); + good.apply_to(&mut settings).expect("valid period applies"); + assert_eq!( + settings.cost_reporting_period, + CostReportingPeriod::MonthToDate + ); +} + +#[test] +fn display_settings_that_affect_tray_trigger_presentation_refresh() { + assert!( + SettingsUpdate { + switcher_shows_icons: Some(false), + ..Default::default() + } + .refreshes_tray_presentation() + ); + assert!( + SettingsUpdate { + reset_time_relative: Some(false), + ..Default::default() + } + .refreshes_tray_presentation() + ); + assert!( + SettingsUpdate { + menu_bar_color_pace: Some(true), + ..Default::default() + } + .refreshes_tray_presentation() + ); + assert!( + SettingsUpdate { + stacked_tray_top_provider: Some("claude".to_string()), + ..Default::default() + } + .refreshes_tray_presentation() + ); +} + +#[test] +fn apply_display_settings_updates_tray_pace_color() { + let mut settings = Settings::default(); + assert!(!settings.menu_bar_color_pace); + + SettingsUpdate { + menu_bar_color_pace: Some(true), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert!(settings.menu_bar_color_pace); + + SettingsUpdate { + menu_bar_color_pace: Some(false), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert!(!settings.menu_bar_color_pace); +} + +#[test] +fn stacked_tray_update_accepts_mode_and_clears_automatic_provider() { + let mut settings = Settings { + stacked_tray_top_provider: Some("codex".to_string()), + ..Settings::default() + }; + + SettingsUpdate { + tray_icon_mode: Some("stacked".to_string()), + stacked_tray_top_provider: Some(String::new()), + stacked_tray_bottom_provider: Some("claude".to_string()), + ..Default::default() + } + .apply_provider_settings(&mut settings); + + assert_eq!(settings.tray_icon_mode, TrayIconMode::Stacked); + assert_eq!(settings.stacked_tray_top_provider, None); + assert_eq!( + settings.stacked_tray_bottom_provider.as_deref(), + Some("claude") + ); +} + +#[test] +fn ui_language_change_refreshes_tray_presentation() { + assert!( + SettingsUpdate { + ui_language: Some("japanese".to_string()), + ..Default::default() + } + .refreshes_tray_presentation() + ); +} + +#[test] +fn apply_display_settings_clamps_window_scale_percent() { + let mut settings = Settings::default(); + + SettingsUpdate { + window_scale_percent: Some(300), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert_eq!(settings.window_scale_percent, 250); + + SettingsUpdate { + window_scale_percent: Some(50), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert_eq!(settings.window_scale_percent, 100); +} + +#[test] +fn apply_display_settings_clamps_tray_scale_percent() { + let mut settings = Settings::default(); + + SettingsUpdate { + tray_scale_percent: Some(300), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert_eq!(settings.tray_scale_percent, 200); + + SettingsUpdate { + tray_scale_percent: Some(50), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert_eq!(settings.tray_scale_percent, 100); +} + +#[test] +fn apply_display_settings_updates_tray_panel_always_on_top() { + let mut settings = Settings::default(); + assert!(!settings.tray_panel_always_on_top); + + SettingsUpdate { + tray_panel_always_on_top: Some(true), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert!(settings.tray_panel_always_on_top); + + SettingsUpdate { + tray_panel_always_on_top: Some(false), + ..Default::default() + } + .apply_display_settings(&mut settings); + assert!(!settings.tray_panel_always_on_top); +} + +#[test] +fn apply_notification_settings_updates_sound() { + let mut settings = Settings::default(); + + SettingsUpdate { + notification_sound_theme: Some(codexbar::settings::NotificationSoundTheme::CodexBar), + ..Default::default() + } + .apply_notification_settings(&mut settings) + .expect("apply sound theme"); + + assert_eq!( + settings.notification_sound_theme, + codexbar::settings::NotificationSoundTheme::CodexBar + ); +} + +#[test] +fn apply_notification_settings_rejects_invalid_custom_sound() { + let mut settings = Settings::default(); + let result = SettingsUpdate { + notification_sound_paths: Some(codexbar::settings::NotificationSoundPaths { + high_usage: Some("relative.wav".to_string()), + ..Default::default() + }), + ..Default::default() + } + .apply_notification_settings(&mut settings); + + assert!(result.is_err()); + assert_eq!( + settings.notification_sound_paths, + codexbar::settings::NotificationSoundPaths::default() + ); +} + +fn switcher_patch(json: &str) -> SettingsUpdate { + serde_json::from_str(&format!(r#"{{"switcherShortcuts":{json}}}"#)).unwrap() +} + +#[test] +fn switcher_shortcuts_patch_stores_normalized_non_default_overrides() { + let mut settings = Settings::default(); + switcher_patch(r#"{"select2":"Alt+Cmd+2","previous":"left","next":"none"}"#) + .apply_to(&mut settings) + .expect("valid overrides are stored"); + + assert_eq!( + settings.switcher_shortcuts, + std::collections::BTreeMap::from([ + ("select2".to_string(), "ctrl+alt+2".to_string()), + ("next".to_string(), "none".to_string()), + ]) + ); +} + +#[test] +fn switcher_shortcuts_empty_patch_restores_defaults() { + let mut settings = Settings::default(); + switcher_patch(r#"{"next":"shift+right"}"#) + .apply_to(&mut settings) + .unwrap(); + switcher_patch("{}").apply_to(&mut settings).unwrap(); + + assert!(settings.switcher_shortcuts.is_empty()); +} + +#[test] +fn switcher_shortcuts_patch_rejects_invalid_maps_and_keeps_stored_value() { + let mut settings = Settings::default(); + switcher_patch(r#"{"next":"shift+right"}"#) + .apply_to(&mut settings) + .unwrap(); + + for (json, message) in [ + ( + r#"{"bogus":"ctrl+1"}"#, + "Unknown switcher shortcut action: bogus", + ), + ( + r#"{"next":"left"}"#, + "Each switcher shortcut can be assigned to only one action", + ), + ( + r#"{"next":"ctrl+r"}"#, + "ctrl+r is reserved and cannot be used as a switcher shortcut", + ), + (r#"{"next":"f1"}"#, "f1 is not a valid switcher shortcut"), + ] { + let error = switcher_patch(json) + .apply_to(&mut settings) + .expect_err(json); + assert_eq!(error, message); + } + assert_eq!( + settings.switcher_shortcuts.get("next").map(String::as_str), + Some("shift+right") + ); +} + +#[test] +fn switcher_shortcuts_snapshot_exposes_the_fully_resolved_map() { + let settings = Settings { + switcher_shortcuts: std::collections::BTreeMap::from([( + "next".to_string(), + "none".to_string(), + )]), + ..Settings::default() + }; + let value = + serde_json::to_value(super::super::bridge::SettingsSnapshot::from(settings)).unwrap(); + let map = &value["switcherShortcuts"]; + + assert_eq!(map["next"], "none"); + assert_eq!(map["previous"], "left"); + assert_eq!(map["select9"], "ctrl+9"); + assert_eq!(map.as_object().unwrap().len(), 11); +} diff --git a/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs b/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs index 2ade811da4c..2b444263255 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs @@ -950,189 +950,4 @@ fn cached_spend( } #[cfg(test)] -mod cache_key_tests { - use super::*; - - fn local_history( - total_tokens: u64, - coverage: codexbar::spend_contract::LocalHistoryCoverage, - known_subtotal_usd: Option, - unpriced: u32, - ) -> codexbar::spend_contract::LocalTokenHistorySummary { - codexbar::spend_contract::LocalTokenHistorySummary { - total_tokens, - session_count: if total_tokens > 0 { 1 } else { 0 }, - coverage, - cost_estimate: codexbar::spend_contract::LocalCostEstimate { - known_subtotal_usd, - coverage: codexbar::spend_contract::CostCoverageCounts { - estimated: if known_subtotal_usd.is_some() { 1 } else { 0 }, - unpriced, - ..Default::default() - }, - ..Default::default() - }, - ..Default::default() - } - } - - #[test] - fn invalidated_owner_clears_orphaned_indexing_activity() { - let mut coordinator = UsageSpendCoordinator::default(); - let owner = coordinator.begin("account:old".to_string()); - - assert!(coordinator.clear_if_indexing(&owner)); - assert!(!coordinator.is_current(&owner)); - } - - #[test] - fn old_owner_cleanup_cannot_clear_a_replacement() { - let mut coordinator = UsageSpendCoordinator::default(); - let old = coordinator.begin("account:old".to_string()); - let replacement = coordinator.begin("account:new".to_string()); - - assert!(!coordinator.clear_if_indexing(&old)); - assert!(coordinator.is_current(&replacement)); - } - - #[test] - fn settings_replacement_preserves_an_intentional_pause() { - let mut coordinator = UsageSpendCoordinator::default(); - let old = coordinator.begin("settings:old".to_string()); - let replacement = coordinator.begin("settings:new".to_string()); - let status = codexbar::core::CachedCostReadStatus { - codex_scan_pause_reason: Some(codexbar::core::CodexScanPauseReason::NoProgress), - ..Default::default() - }; - mark_refresh_paused_if_codex_scan_paused( - &mut coordinator, - &replacement, - true, - status.codex_scan_pause_reason.as_ref(), - ); - - assert!(!coordinator.clear_if_indexing(&old)); - assert_eq!( - coordinator.current.as_ref().map(|(_, phase)| *phase), - Some(UsageSpendRefreshPhase::Paused) - ); - } - - fn empty_summary() -> UsageSpendSummary { - UsageSpendSummary { - reporting_period: "rolling:30".to_string(), - rows: Vec::new(), - contract: SpendContract { - provider_id: "codex".to_string(), - reporting_period: "rolling:30".to_string(), - history_days: 30, - known_cost_usd: None, - known_zero: false, - provenance: codexbar::spend_contract::CostProvenance::Unknown, - price_coverage: Default::default(), - price_coverage_ratio: None, - history_coverage_established: false, - token_mix: Default::default(), - token_total: None, - conversation_count: 0, - models: Vec::new(), - projects: Vec::new(), - conversations: Vec::new(), - daily: Vec::new(), - hourly_activity: Vec::new(), - project_source_status: None, - custom_pricing_active: false, - imports: Vec::new(), - }, - reporting_day: "2026-10-01".to_string(), - dashboard_timezone: "UTC".to_string(), - } - } - - #[test] - fn only_a_summary_rebuild_may_refresh_opencodex_pricing() { - let mut coordinator = UsageSpendCoordinator::default(); - assert!(coordinator.reusable("day|30", false).is_none()); - coordinator.cache = Some(CachedUsageSpendSummary { - key: "day|30".to_string(), - summary: empty_summary(), - refresh_owner: None, - }); - // A cached read starts no network work; a forced or new key rebuilds. - assert!(coordinator.reusable("day|30", false).is_some()); - assert!(coordinator.reusable("day|30", true).is_none()); - assert!(coordinator.reusable("day|7", false).is_none()); - } - - #[test] - fn privacy_mode_is_part_of_usage_spend_cache_identity() { - let public = usage_spend_cache_key_with_privacy(&[], "rolling:30", false, false, false); - let private = usage_spend_cache_key_with_privacy(&[], "rolling:30", false, false, true); - assert_ne!(public, private); - } - - #[test] - fn pi_history_is_an_alternate_view_not_a_shared_overview_source() { - assert!(!include_in_shared_overview("pi", true, true)); - assert!(include_in_shared_overview("codex", true, false)); - assert!(include_in_shared_overview("claude", false, true)); - assert!(!include_in_shared_overview("codex", false, false)); - } - - #[test] - fn antigravity_partial_history_exposes_only_the_known_subtotal() { - use codexbar::spend_contract::LocalHistoryCoverage; - - let seven = local_history(100, LocalHistoryCoverage::Partial, Some(1.25), 0); - let thirty = local_history(200, LocalHistoryCoverage::Partial, Some(2.50), 0); - let spend = antigravity_spend_values( - cached_spend(None, CostReportingPeriod::Rolling(30), chrono::Utc::now()), - &seven, - &thirty, - ); - - assert_eq!(spend.seven_day, None); - assert_eq!(spend.thirty_day, None); - assert_eq!(spend.seven_day_tokens, None); - assert_eq!(spend.thirty_day_tokens, None); - assert!(spend.source.contains("known API list-price subtotal")); - } - - #[test] - fn antigravity_lower_bound_history_publishes_floors_not_exact_totals() { - use codexbar::spend_contract::LocalHistoryCoverage; - - let mut seven = local_history(100, LocalHistoryCoverage::Partial, Some(1.25), 0); - seven.lower_bound = true; - let withheld = local_history(0, LocalHistoryCoverage::Partial, None, 0); - let spend = antigravity_spend_values( - cached_spend(None, CostReportingPeriod::Rolling(30), chrono::Utc::now()), - &seven, - &withheld, - ); - - assert_eq!(spend.seven_day, None); - assert_eq!(spend.seven_day_tokens, Some(100)); - assert_eq!(spend.thirty_day, None); - assert_eq!(spend.thirty_day_tokens, None); - } - - #[test] - fn antigravity_complete_empty_history_is_a_known_zero() { - use codexbar::spend_contract::LocalHistoryCoverage; - - let seven = local_history(0, LocalHistoryCoverage::Complete, None, 0); - let thirty = local_history(0, LocalHistoryCoverage::Complete, None, 0); - let spend = antigravity_spend_values( - cached_spend(None, CostReportingPeriod::Rolling(30), chrono::Utc::now()), - &seven, - &thirty, - ); - - assert_eq!(spend.seven_day, Some(0.0)); - assert_eq!(spend.thirty_day, Some(0.0)); - assert_eq!(spend.seven_day_tokens, Some(0)); - assert_eq!(spend.thirty_day_tokens, Some(0)); - assert!(spend.source.contains("API list-price estimate")); - } -} +mod cache_key_tests; diff --git a/apps/desktop-tauri/src-tauri/src/commands/usage_spend/cache_key_tests.rs b/apps/desktop-tauri/src-tauri/src/commands/usage_spend/cache_key_tests.rs new file mode 100644 index 00000000000..a48e117361e --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/usage_spend/cache_key_tests.rs @@ -0,0 +1,184 @@ +use super::*; + +fn local_history( + total_tokens: u64, + coverage: codexbar::spend_contract::LocalHistoryCoverage, + known_subtotal_usd: Option, + unpriced: u32, +) -> codexbar::spend_contract::LocalTokenHistorySummary { + codexbar::spend_contract::LocalTokenHistorySummary { + total_tokens, + session_count: if total_tokens > 0 { 1 } else { 0 }, + coverage, + cost_estimate: codexbar::spend_contract::LocalCostEstimate { + known_subtotal_usd, + coverage: codexbar::spend_contract::CostCoverageCounts { + estimated: if known_subtotal_usd.is_some() { 1 } else { 0 }, + unpriced, + ..Default::default() + }, + ..Default::default() + }, + ..Default::default() + } +} + +#[test] +fn invalidated_owner_clears_orphaned_indexing_activity() { + let mut coordinator = UsageSpendCoordinator::default(); + let owner = coordinator.begin("account:old".to_string()); + + assert!(coordinator.clear_if_indexing(&owner)); + assert!(!coordinator.is_current(&owner)); +} + +#[test] +fn old_owner_cleanup_cannot_clear_a_replacement() { + let mut coordinator = UsageSpendCoordinator::default(); + let old = coordinator.begin("account:old".to_string()); + let replacement = coordinator.begin("account:new".to_string()); + + assert!(!coordinator.clear_if_indexing(&old)); + assert!(coordinator.is_current(&replacement)); +} + +#[test] +fn settings_replacement_preserves_an_intentional_pause() { + let mut coordinator = UsageSpendCoordinator::default(); + let old = coordinator.begin("settings:old".to_string()); + let replacement = coordinator.begin("settings:new".to_string()); + let status = codexbar::core::CachedCostReadStatus { + codex_scan_pause_reason: Some(codexbar::core::CodexScanPauseReason::NoProgress), + ..Default::default() + }; + mark_refresh_paused_if_codex_scan_paused( + &mut coordinator, + &replacement, + true, + status.codex_scan_pause_reason.as_ref(), + ); + + assert!(!coordinator.clear_if_indexing(&old)); + assert_eq!( + coordinator.current.as_ref().map(|(_, phase)| *phase), + Some(UsageSpendRefreshPhase::Paused) + ); +} + +fn empty_summary() -> UsageSpendSummary { + UsageSpendSummary { + reporting_period: "rolling:30".to_string(), + rows: Vec::new(), + contract: SpendContract { + provider_id: "codex".to_string(), + reporting_period: "rolling:30".to_string(), + history_days: 30, + known_cost_usd: None, + known_zero: false, + provenance: codexbar::spend_contract::CostProvenance::Unknown, + price_coverage: Default::default(), + price_coverage_ratio: None, + history_coverage_established: false, + token_mix: Default::default(), + token_total: None, + conversation_count: 0, + models: Vec::new(), + projects: Vec::new(), + conversations: Vec::new(), + daily: Vec::new(), + hourly_activity: Vec::new(), + project_source_status: None, + custom_pricing_active: false, + imports: Vec::new(), + }, + reporting_day: "2026-10-01".to_string(), + dashboard_timezone: "UTC".to_string(), + } +} + +#[test] +fn only_a_summary_rebuild_may_refresh_opencodex_pricing() { + let mut coordinator = UsageSpendCoordinator::default(); + assert!(coordinator.reusable("day|30", false).is_none()); + coordinator.cache = Some(CachedUsageSpendSummary { + key: "day|30".to_string(), + summary: empty_summary(), + refresh_owner: None, + }); + // A cached read starts no network work; a forced or new key rebuilds. + assert!(coordinator.reusable("day|30", false).is_some()); + assert!(coordinator.reusable("day|30", true).is_none()); + assert!(coordinator.reusable("day|7", false).is_none()); +} + +#[test] +fn privacy_mode_is_part_of_usage_spend_cache_identity() { + let public = usage_spend_cache_key_with_privacy(&[], "rolling:30", false, false, false); + let private = usage_spend_cache_key_with_privacy(&[], "rolling:30", false, false, true); + assert_ne!(public, private); +} + +#[test] +fn pi_history_is_an_alternate_view_not_a_shared_overview_source() { + assert!(!include_in_shared_overview("pi", true, true)); + assert!(include_in_shared_overview("codex", true, false)); + assert!(include_in_shared_overview("claude", false, true)); + assert!(!include_in_shared_overview("codex", false, false)); +} + +#[test] +fn antigravity_partial_history_exposes_only_the_known_subtotal() { + use codexbar::spend_contract::LocalHistoryCoverage; + + let seven = local_history(100, LocalHistoryCoverage::Partial, Some(1.25), 0); + let thirty = local_history(200, LocalHistoryCoverage::Partial, Some(2.50), 0); + let spend = antigravity_spend_values( + cached_spend(None, CostReportingPeriod::Rolling(30), chrono::Utc::now()), + &seven, + &thirty, + ); + + assert_eq!(spend.seven_day, None); + assert_eq!(spend.thirty_day, None); + assert_eq!(spend.seven_day_tokens, None); + assert_eq!(spend.thirty_day_tokens, None); + assert!(spend.source.contains("known API list-price subtotal")); +} + +#[test] +fn antigravity_lower_bound_history_publishes_floors_not_exact_totals() { + use codexbar::spend_contract::LocalHistoryCoverage; + + let mut seven = local_history(100, LocalHistoryCoverage::Partial, Some(1.25), 0); + seven.lower_bound = true; + let withheld = local_history(0, LocalHistoryCoverage::Partial, None, 0); + let spend = antigravity_spend_values( + cached_spend(None, CostReportingPeriod::Rolling(30), chrono::Utc::now()), + &seven, + &withheld, + ); + + assert_eq!(spend.seven_day, None); + assert_eq!(spend.seven_day_tokens, Some(100)); + assert_eq!(spend.thirty_day, None); + assert_eq!(spend.thirty_day_tokens, None); +} + +#[test] +fn antigravity_complete_empty_history_is_a_known_zero() { + use codexbar::spend_contract::LocalHistoryCoverage; + + let seven = local_history(0, LocalHistoryCoverage::Complete, None, 0); + let thirty = local_history(0, LocalHistoryCoverage::Complete, None, 0); + let spend = antigravity_spend_values( + cached_spend(None, CostReportingPeriod::Rolling(30), chrono::Utc::now()), + &seven, + &thirty, + ); + + assert_eq!(spend.seven_day, Some(0.0)); + assert_eq!(spend.thirty_day, Some(0.0)); + assert_eq!(spend.seven_day_tokens, Some(0)); + assert_eq!(spend.thirty_day_tokens, Some(0)); + assert!(spend.source.contains("API list-price estimate")); +} diff --git a/apps/desktop-tauri/src-tauri/src/floatbar/window.rs b/apps/desktop-tauri/src-tauri/src/floatbar/window.rs index e1d28bc137d..e5981028404 100644 --- a/apps/desktop-tauri/src-tauri/src/floatbar/window.rs +++ b/apps/desktop-tauri/src-tauri/src/floatbar/window.rs @@ -668,178 +668,4 @@ unsafe extern "system" { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn opacity_to_alpha_clamps_low_values() { - assert_eq!(opacity_to_alpha(0), opacity_to_alpha(30)); - assert_eq!(opacity_to_alpha(10), opacity_to_alpha(30)); - } - - #[test] - fn opacity_to_alpha_full_is_255() { - assert_eq!(opacity_to_alpha(100), 255); - } - - #[test] - fn opacity_to_alpha_is_monotonic() { - let a = opacity_to_alpha(30); - let b = opacity_to_alpha(60); - let c = opacity_to_alpha(100); - assert!(a < b); - assert!(b < c); - } - - #[test] - fn opacity_to_alpha_midpoint() { - // 50% should be roughly half of 255. - let alpha = opacity_to_alpha(50); - assert!((125..=130).contains(&alpha), "got {alpha}"); - } - - #[test] - fn initial_size_picks_orientation() { - assert_eq!( - initial_size("horizontal"), - (FLOATBAR_DEFAULT_WIDTH_H, FLOATBAR_DEFAULT_HEIGHT_H) - ); - assert_eq!( - initial_size("vertical"), - (FLOATBAR_DEFAULT_WIDTH_V, FLOATBAR_DEFAULT_HEIGHT_V) - ); - // Unknown values fall through to horizontal so a corrupted setting - // can't yield an unreadable strip. - assert_eq!( - initial_size("diagonal"), - (FLOATBAR_DEFAULT_WIDTH_H, FLOATBAR_DEFAULT_HEIGHT_H) - ); - } - - #[test] - fn windows_minimized_position_is_not_restored() { - assert!(is_windows_minimized_position(-32_000, -32_000)); - } - - #[test] - fn legitimate_negative_monitor_positions_are_preserved() { - assert!(!is_windows_minimized_position(-3_840, 0)); - assert!(!is_windows_minimized_position(-8_000, -8_000)); - assert!(!is_windows_minimized_position(-16_000, -16_000)); - } - - #[test] - fn minimized_or_parked_physical_positions_are_not_remembered() { - assert!(!should_remember_physical_position(true, 100, 100)); - assert!(!should_remember_physical_position(false, -32_000, -32_000)); - assert!(should_remember_physical_position(false, -3_840, 100)); - } - - #[test] - fn physical_multi_monitor_origin_is_remembered() { - assert!(should_remember_physical_position(false, -8_000, -8_000)); - } - - #[test] - fn window_must_intersect_an_active_monitor_to_be_visible() { - let window_size = PhysicalSize::new(211, 40); - let monitor_size = PhysicalSize::new(1_920, 1_032); - - assert!(physical_rects_intersect( - PhysicalPosition::new(780, 8), - window_size, - PhysicalPosition::new(0, 0), - monitor_size, - )); - assert!(!physical_rects_intersect( - PhysicalPosition::new(-32_000, -32_000), - window_size, - PhysicalPosition::new(0, 0), - monitor_size, - )); - assert!(physical_rects_intersect( - PhysicalPosition::new(-3_840, 8), - window_size, - PhysicalPosition::new(-3_840, 0), - monitor_size, - )); - } - - #[test] - fn taskbar_strip_outside_work_area_still_counts_as_on_monitor() { - // Full monitor 1920x1080; work area is 1920x1032 (48px taskbar strip). - // A taskbar-style bar sitting fully in the strip is on-screen for - // visibility even though it has zero work-area intersection. - let window_pos = PhysicalPosition::new(800, 1_040); - let window_size = PhysicalSize::new(211, 40); - let monitor_pos = PhysicalPosition::new(0, 0); - let monitor_size = PhysicalSize::new(1_920, 1_080); - let work_area_pos = PhysicalPosition::new(0, 0); - let work_area_size = PhysicalSize::new(1_920, 1_032); - - assert!(physical_rects_intersect( - window_pos, - window_size, - monitor_pos, - monitor_size, - )); - assert!(!physical_rects_intersect( - window_pos, - window_size, - work_area_pos, - work_area_size, - )); - } - - #[test] - fn hidpi_parked_logical_geometry_is_rejected() { - assert!(is_unusable_stored_logical_position(-32_000, -32_000)); - // -32000 physical at 1.25x / 1.5x / 2.0x scale factors. - assert!(is_unusable_stored_logical_position(-25_600, -25_600)); - assert!(is_unusable_stored_logical_position(-21_333, -21_333)); - assert!(is_unusable_stored_logical_position(-16_000, -16_000)); - // Legitimate multi-monitor logical origins must still restore. - assert!(!is_unusable_stored_logical_position(-3_840, 0)); - assert!(!is_unusable_stored_logical_position(-8_000, -8_000)); - } - - #[test] - fn disconnected_monitor_position_falls_back_to_primary_work_area() { - let work_area_position = PhysicalPosition::new(0, 0); - let work_area_size = PhysicalSize::new(1_920, 1_032); - let window_size = PhysicalSize::new(211, 40); - - assert_eq!( - fallback_physical_position( - work_area_position, - work_area_size, - window_size, - 1.0, - "floating", - ), - PhysicalPosition::new(855, 8), - ); - assert_eq!( - fallback_physical_position( - work_area_position, - work_area_size, - window_size, - 1.0, - "taskbar", - ), - PhysicalPosition::new(855, 984), - ); - } - - #[test] - fn default_logical_origin_matches_first_open_policy() { - assert_eq!( - default_logical_origin(0.0, 0.0, 1_920.0, 1_080.0, 211.0, 40.0, "floating"), - (854.5, 8.0), - ); - assert_eq!( - default_logical_origin(0.0, 0.0, 1_920.0, 1_080.0, 211.0, 40.0, "taskbar"), - (854.5, 1_032.0), - ); - } -} +mod tests; diff --git a/apps/desktop-tauri/src-tauri/src/floatbar/window/tests.rs b/apps/desktop-tauri/src-tauri/src/floatbar/window/tests.rs new file mode 100644 index 00000000000..8b3ad8bac54 --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/floatbar/window/tests.rs @@ -0,0 +1,173 @@ +use super::*; + +#[test] +fn opacity_to_alpha_clamps_low_values() { + assert_eq!(opacity_to_alpha(0), opacity_to_alpha(30)); + assert_eq!(opacity_to_alpha(10), opacity_to_alpha(30)); +} + +#[test] +fn opacity_to_alpha_full_is_255() { + assert_eq!(opacity_to_alpha(100), 255); +} + +#[test] +fn opacity_to_alpha_is_monotonic() { + let a = opacity_to_alpha(30); + let b = opacity_to_alpha(60); + let c = opacity_to_alpha(100); + assert!(a < b); + assert!(b < c); +} + +#[test] +fn opacity_to_alpha_midpoint() { + // 50% should be roughly half of 255. + let alpha = opacity_to_alpha(50); + assert!((125..=130).contains(&alpha), "got {alpha}"); +} + +#[test] +fn initial_size_picks_orientation() { + assert_eq!( + initial_size("horizontal"), + (FLOATBAR_DEFAULT_WIDTH_H, FLOATBAR_DEFAULT_HEIGHT_H) + ); + assert_eq!( + initial_size("vertical"), + (FLOATBAR_DEFAULT_WIDTH_V, FLOATBAR_DEFAULT_HEIGHT_V) + ); + // Unknown values fall through to horizontal so a corrupted setting + // can't yield an unreadable strip. + assert_eq!( + initial_size("diagonal"), + (FLOATBAR_DEFAULT_WIDTH_H, FLOATBAR_DEFAULT_HEIGHT_H) + ); +} + +#[test] +fn windows_minimized_position_is_not_restored() { + assert!(is_windows_minimized_position(-32_000, -32_000)); +} + +#[test] +fn legitimate_negative_monitor_positions_are_preserved() { + assert!(!is_windows_minimized_position(-3_840, 0)); + assert!(!is_windows_minimized_position(-8_000, -8_000)); + assert!(!is_windows_minimized_position(-16_000, -16_000)); +} + +#[test] +fn minimized_or_parked_physical_positions_are_not_remembered() { + assert!(!should_remember_physical_position(true, 100, 100)); + assert!(!should_remember_physical_position(false, -32_000, -32_000)); + assert!(should_remember_physical_position(false, -3_840, 100)); +} + +#[test] +fn physical_multi_monitor_origin_is_remembered() { + assert!(should_remember_physical_position(false, -8_000, -8_000)); +} + +#[test] +fn window_must_intersect_an_active_monitor_to_be_visible() { + let window_size = PhysicalSize::new(211, 40); + let monitor_size = PhysicalSize::new(1_920, 1_032); + + assert!(physical_rects_intersect( + PhysicalPosition::new(780, 8), + window_size, + PhysicalPosition::new(0, 0), + monitor_size, + )); + assert!(!physical_rects_intersect( + PhysicalPosition::new(-32_000, -32_000), + window_size, + PhysicalPosition::new(0, 0), + monitor_size, + )); + assert!(physical_rects_intersect( + PhysicalPosition::new(-3_840, 8), + window_size, + PhysicalPosition::new(-3_840, 0), + monitor_size, + )); +} + +#[test] +fn taskbar_strip_outside_work_area_still_counts_as_on_monitor() { + // Full monitor 1920x1080; work area is 1920x1032 (48px taskbar strip). + // A taskbar-style bar sitting fully in the strip is on-screen for + // visibility even though it has zero work-area intersection. + let window_pos = PhysicalPosition::new(800, 1_040); + let window_size = PhysicalSize::new(211, 40); + let monitor_pos = PhysicalPosition::new(0, 0); + let monitor_size = PhysicalSize::new(1_920, 1_080); + let work_area_pos = PhysicalPosition::new(0, 0); + let work_area_size = PhysicalSize::new(1_920, 1_032); + + assert!(physical_rects_intersect( + window_pos, + window_size, + monitor_pos, + monitor_size, + )); + assert!(!physical_rects_intersect( + window_pos, + window_size, + work_area_pos, + work_area_size, + )); +} + +#[test] +fn hidpi_parked_logical_geometry_is_rejected() { + assert!(is_unusable_stored_logical_position(-32_000, -32_000)); + // -32000 physical at 1.25x / 1.5x / 2.0x scale factors. + assert!(is_unusable_stored_logical_position(-25_600, -25_600)); + assert!(is_unusable_stored_logical_position(-21_333, -21_333)); + assert!(is_unusable_stored_logical_position(-16_000, -16_000)); + // Legitimate multi-monitor logical origins must still restore. + assert!(!is_unusable_stored_logical_position(-3_840, 0)); + assert!(!is_unusable_stored_logical_position(-8_000, -8_000)); +} + +#[test] +fn disconnected_monitor_position_falls_back_to_primary_work_area() { + let work_area_position = PhysicalPosition::new(0, 0); + let work_area_size = PhysicalSize::new(1_920, 1_032); + let window_size = PhysicalSize::new(211, 40); + + assert_eq!( + fallback_physical_position( + work_area_position, + work_area_size, + window_size, + 1.0, + "floating", + ), + PhysicalPosition::new(855, 8), + ); + assert_eq!( + fallback_physical_position( + work_area_position, + work_area_size, + window_size, + 1.0, + "taskbar", + ), + PhysicalPosition::new(855, 984), + ); +} + +#[test] +fn default_logical_origin_matches_first_open_policy() { + assert_eq!( + default_logical_origin(0.0, 0.0, 1_920.0, 1_080.0, 211.0, 40.0, "floating"), + (854.5, 8.0), + ); + assert_eq!( + default_logical_origin(0.0, 0.0, 1_920.0, 1_080.0, 211.0, 40.0, "taskbar"), + (854.5, 1_032.0), + ); +} diff --git a/apps/desktop-tauri/src-tauri/src/usage_metric.rs b/apps/desktop-tauri/src-tauri/src/usage_metric.rs index 78eac02a122..bc99d1fbd51 100644 --- a/apps/desktop-tauri/src-tauri/src/usage_metric.rs +++ b/apps/desktop-tauri/src-tauri/src/usage_metric.rs @@ -444,755 +444,4 @@ fn automatic_window_is_exhausted(window: &RateWindowSnapshot) -> bool { } #[cfg(test)] -mod tests { - use super::*; - use codexbar::providers::copilot::SEAT_CREDIT_WINDOW_ID; - - fn window(used_percent: f64) -> RateWindowSnapshot { - derived_window(used_percent, None) - } - - fn snapshot() -> ProviderUsageSnapshot { - ProviderUsageSnapshot { - provider_id: "codex".to_string(), - display_name: "Codex".to_string(), - primary: window(20.0), - secondary: Some(window(60.0)), - source_label: "test".to_string(), - updated_at: "2026-08-16T00:00:00Z".to_string(), - ..Default::default() - } - } - - #[test] - fn weekly_preference_selects_the_weekly_window() { - let snapshot = snapshot(); - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Codex, MetricPreference::Weekly); - - assert_eq!( - selected_usage_window(&snapshot, &settings).used_percent, - 60.0 - ); - } - - #[test] - fn missing_selected_session_falls_back_to_a_real_window() { - let mut snapshot = snapshot(); - snapshot.primary.is_informational = true; - snapshot.primary.used_percent = 0.0; - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Codex, MetricPreference::Session); - - assert_eq!( - selected_usage_window(&snapshot, &settings).used_percent, - 60.0 - ); - } - - #[test] - fn automatic_selects_the_highest_real_window() { - let snapshot = snapshot(); - - assert_eq!( - selected_usage_window(&snapshot, &Settings::default()).used_percent, - 60.0 - ); - } - - #[test] - fn copilot_automatic_uses_seat_credit_progress_without_metered_quota() { - let mut snapshot = snapshot(); - snapshot.provider_id = "copilot".to_string(); - snapshot.primary = RateWindowSnapshot { - is_informational: true, - ..window(0.0) - }; - snapshot.secondary = None; - snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { - id: SEAT_CREDIT_WINDOW_ID.to_string(), - title: "Credits used".to_string(), - window: window(35.0), - fallback_lane: true, - icon_fallback: None, - }]; - - assert_eq!( - selected_usage_window(&snapshot, &Settings::default()).used_percent, - 35.0 - ); - } - - #[test] - fn copilot_automatic_keeps_metered_quota_authoritative_over_seat_credits() { - let mut snapshot = snapshot(); - snapshot.provider_id = "copilot".to_string(); - snapshot.primary = window(20.0); - snapshot.secondary = None; - snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { - id: SEAT_CREDIT_WINDOW_ID.to_string(), - title: "Credits used".to_string(), - window: window(90.0), - fallback_lane: true, - icon_fallback: None, - }]; - - assert_eq!( - selected_usage_window(&snapshot, &Settings::default()).used_percent, - 20.0 - ); - } - - #[test] - fn copilot_explicit_session_does_not_fall_back_to_seat_credits() { - let mut snapshot = snapshot(); - snapshot.provider_id = "copilot".to_string(); - snapshot.primary = RateWindowSnapshot { - is_informational: true, - ..window(0.0) - }; - snapshot.secondary = None; - snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { - id: SEAT_CREDIT_WINDOW_ID.to_string(), - title: "Credits used".to_string(), - window: window(35.0), - fallback_lane: true, - icon_fallback: None, - }]; - let mut settings = Settings::default(); - let provider = ProviderId::from_cli_name(&snapshot.provider_id).expect("copilot provider"); - settings.set_provider_metric(provider, MetricPreference::Session); - - let selected = selected_usage_window(&snapshot, &settings); - assert!(selected.is_informational); - assert_eq!(selected.used_percent, 0.0); - } - - #[test] - fn cursor_automatic_uses_semantic_monthly_lane_and_keeps_grok_bot_explicit() { - let mut snapshot = snapshot(); - snapshot.provider_id = "cursor".to_string(); - snapshot.primary = window(85.0); - snapshot.secondary = Some(window(20.0)); - snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { - id: "cursor-grok-bot".to_string(), - title: "Grok Bot".to_string(), - window: window(95.0), - fallback_lane: false, - icon_fallback: None, - }]; - - assert_eq!( - selected_usage_window(&snapshot, &Settings::default()).used_percent, - 20.0 - ); - - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Cursor, MetricPreference::ExtraUsage); - assert_eq!( - selected_usage_window(&snapshot, &settings).used_percent, - 95.0 - ); - } - - #[test] - fn kimi_monthly_only_snapshot_selects_the_total_usage_lane() { - // Upstream 0.60.5 #3694: a Code API response may report only the - // monthly Total usage pool. The weekly lane is then an informational - // placeholder, and every metric preference lands on the monthly lane. - let mut snapshot = snapshot(); - snapshot.provider_id = "kimi".to_string(); - snapshot.primary = RateWindowSnapshot { - is_informational: true, - ..window(0.0) - }; - snapshot.secondary = None; - snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { - id: "kimi-monthly".to_string(), - title: "Total usage".to_string(), - window: window(100.0), - fallback_lane: false, - icon_fallback: None, - }]; - - for preference in [ - MetricPreference::Automatic, - MetricPreference::Session, - MetricPreference::Weekly, - ] { - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Kimi, preference); - let selected = selected_usage_window(&snapshot, &settings); - assert!(!selected.is_informational, "{preference:?}"); - assert_eq!(selected.used_percent, 100.0, "{preference:?}"); - } - } - - #[test] - fn opencodego_automatic_prefers_explicitly_exhausted_window_over_higher_percentage() { - let mut snapshot = snapshot(); - snapshot.provider_id = "opencodego".to_string(); - snapshot.primary.is_exhausted = true; - - let selected = selected_usage_window(&snapshot, &Settings::default()); - - assert_eq!(selected.used_percent, 20.0); - assert!(selected.is_exhausted); - } - - #[test] - fn claude_and_codex_automatic_keep_highest_used_window() { - for provider_id in ["claude", "codex"] { - let mut snapshot = snapshot(); - snapshot.provider_id = provider_id.to_string(); - snapshot.primary.is_exhausted = true; - - let selected = selected_usage_window(&snapshot, &Settings::default()); - - assert_eq!( - selected.used_percent, 60.0, - "{provider_id} should keep highest-used automatic selection" - ); - assert!(!selected.is_exhausted); - } - } - - #[test] - fn automatic_treats_a_full_window_as_exhausted_even_without_the_flag() { - let mut snapshot = snapshot(); - let mut full = window(100.0); - full.is_exhausted = false; - snapshot.tertiary = Some(full); - - let selected = selected_usage_window(&snapshot, &Settings::default()); - - assert_eq!(selected.used_percent, 100.0); - assert!(!selected.is_exhausted); - } - - #[test] - fn non_automatic_highest_window_keeps_percentage_order() { - let healthy = window(80.0); - let mut exhausted = window(20.0); - exhausted.is_exhausted = true; - - let selected = highest_window([&healthy, &exhausted].into_iter()).expect("window"); - - assert_eq!(selected.used_percent, 80.0); - } - - #[test] - fn antigravity_automatic_prefers_active_core_quota_over_exhausted_extra_window() { - let mut snapshot = snapshot(); - snapshot.provider_id = "antigravity".to_string(); - snapshot.primary = window(100.0); - snapshot.primary.is_exhausted = true; - snapshot.primary_label = Some("Gemini 5h".to_string()); - snapshot.secondary = Some(window(88.0)); - snapshot.secondary_label = Some("Gemini Weekly".to_string()); - snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { - id: "antigravity-quota-summary-3p-weekly".to_string(), - title: "Claude/GPT weekly".to_string(), - window: window(100.0), - fallback_lane: false, - icon_fallback: None, - }]; - - let selected = selected_usage_window(&snapshot, &Settings::default()); - - assert_eq!(selected.used_percent, 88.0); - assert!(!selected.is_exhausted); - } - - #[test] - fn antigravity_automatic_uses_core_slots_only() { - let mut snapshot = snapshot(); - snapshot.provider_id = "antigravity".to_string(); - snapshot.primary = window(80.0); - snapshot.secondary = Some(window(20.0)); - snapshot.model_specific = Some(window(90.0)); - snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { - id: "legacy-other".to_string(), - title: "Other".to_string(), - window: window(100.0), - fallback_lane: false, - icon_fallback: None, - }]; - - let selected = selected_usage_window(&snapshot, &Settings::default()); - - assert_eq!(selected.used_percent, 90.0); - } - - #[test] - fn single_meaningful_quota_omits_the_companion_icon_lane() { - let mut snapshot = snapshot(); - snapshot - .secondary - .as_mut() - .expect("fixture has a secondary window") - .is_informational = true; - - let (selected, companion) = selected_usage_icon_windows(&snapshot, &Settings::default()); - - assert_eq!(selected.used_percent, 20.0); - assert!(companion.is_none()); - } - - #[test] - fn average_preference_derives_the_combined_percentage() { - let mut snapshot = snapshot(); - snapshot.provider_id = "gemini".to_string(); - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Gemini, MetricPreference::Average); - - let selected = selected_usage_window(&snapshot, &settings); - assert_eq!(selected.used_percent, 40.0); - assert_eq!(selected.remaining_percent, 60.0); - } - - fn mistral_snapshot(plan: Option) -> ProviderUsageSnapshot { - let mut snapshot = snapshot(); - snapshot.provider_id = "mistral".to_string(); - snapshot.display_name = "Mistral".to_string(); - snapshot.primary = window(2.0); - snapshot.primary_label = Some("Included API".to_string()); - snapshot.secondary = None; - snapshot.extra_rate_windows = plan - .map(|window| crate::commands::NamedRateWindowSnapshot { - id: codexbar::providers::mistral::MONTHLY_PLAN_WINDOW_ID.to_string(), - title: "Monthly Plan".to_string(), - window, - fallback_lane: false, - icon_fallback: None, - }) - .into_iter() - .collect(); - snapshot - } - - fn metric_settings(provider: ProviderId, preference: MetricPreference) -> Settings { - let mut settings = Settings::default(); - settings.set_provider_metric(provider, preference); - settings - } - - #[test] - fn mistral_monthly_plan_selects_the_vibe_plan_window() { - let mut snapshot = mistral_snapshot(Some(window(42.0))); - snapshot.primary = window(80.0); - - let monthly_plan = metric_settings(ProviderId::Mistral, MetricPreference::MonthlyPlan); - assert_eq!( - selected_usage_window(&snapshot, &monthly_plan).used_percent, - 42.0 - ); - // Included API and Automatic stay separate choices (upstream #4072). - let included_api = metric_settings(ProviderId::Mistral, MetricPreference::Session); - assert_eq!( - selected_usage_window(&snapshot, &included_api).used_percent, - 80.0 - ); - assert_eq!( - selected_usage_window(&snapshot, &Settings::default()).used_percent, - 80.0 - ); - let presentation = - crate::commands::ProviderUsagePresentationSnapshot::new(snapshot, &monthly_plan); - assert_eq!(presentation.selected_metric.used_percent, 42.0); - } - - #[test] - fn mistral_monthly_plan_falls_back_to_included_api_without_a_known_plan() { - let settings = metric_settings(ProviderId::Mistral, MetricPreference::MonthlyPlan); - let unknown_plan = RateWindowSnapshot { - is_informational: true, - ..window(0.0) - }; - - for snapshot in [mistral_snapshot(None), mistral_snapshot(Some(unknown_plan))] { - let selected = selected_usage_window(&snapshot, &settings); - assert_eq!(selected.used_percent, 2.0); - assert!(!selected.is_informational); - } - } - - #[test] - fn mistral_monthly_plan_never_selects_a_spend_window() { - let mut snapshot = mistral_snapshot(None); - snapshot.cost = Some(crate::commands::CostSnapshotBridge { - used: 45.0, - limit: Some(50.0), - remaining: Some(5.0), - currency_code: "EUR".to_string(), - currency_symbol: Some("€".to_string()), - period: "month".to_string(), - resets_at: None, - formatted_used: "€45.00".to_string(), - formatted_limit: Some("€50.00".to_string()), - balance: None, - balance_updated_at: None, - account_id: None, - formatted_balance: None, - daily: Vec::new(), - always_visible: false, - }); - let settings = metric_settings(ProviderId::Mistral, MetricPreference::MonthlyPlan); - - assert_eq!( - selected_usage_window(&snapshot, &settings).used_percent, - 2.0 - ); - } - - #[test] - fn monthly_plan_without_a_provider_plan_window_falls_through_to_automatic() { - let mut snapshot = snapshot(); - snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { - id: codexbar::providers::mistral::MONTHLY_PLAN_WINDOW_ID.to_string(), - title: "Monthly Plan".to_string(), - window: window(90.0), - fallback_lane: false, - icon_fallback: None, - }]; - let settings = metric_settings(ProviderId::Codex, MetricPreference::MonthlyPlan); - - // Codex declares no plan window, so a same-named lane is not special - // and the choice resolves like Automatic (highest real window). - assert_eq!( - selected_usage_window(&snapshot, &settings).used_percent, - selected_usage_window(&snapshot, &Settings::default()).used_percent - ); - assert!(monthly_plan_window(&snapshot, Some(ProviderId::Codex)).is_none()); - } - - #[test] - fn presentation_payload_flattens_the_snapshot_and_selected_metric() { - let presentation = crate::commands::ProviderUsagePresentationSnapshot::new( - snapshot(), - &Settings::default(), - ); - let value = serde_json::to_value(presentation).expect("serialize presentation"); - - assert_eq!(value["providerId"], "codex"); - assert_eq!(value["selectedMetric"]["usedPercent"], 60.0); - assert!(value.get("snapshot").is_none()); - } - - #[test] - fn hidden_usage_items_are_presentation_metadata_and_do_not_change_selected_metric() { - let mut settings = Settings::default(); - settings.set_hidden_usage_item_ids(ProviderId::Codex, vec!["metric:secondary".to_string()]); - - let presentation = - crate::commands::ProviderUsagePresentationSnapshot::new(snapshot(), &settings); - - assert!(presentation.snapshot.secondary.is_some()); - assert_eq!(presentation.selected_metric.used_percent, 60.0); - assert_eq!( - presentation.hidden_usage_item_ids, - vec!["metric:secondary".to_string()] - ); - let value = serde_json::to_value(presentation).expect("serialize presentation"); - assert_eq!( - value["hiddenUsageItemIds"], - serde_json::json!(["metric:secondary"]) - ); - } - - #[test] - fn claude_routines_hide_and_restore_preserve_raw_data_and_selected_metric() { - let mut raw = snapshot(); - raw.provider_id = "claude".to_string(); - raw.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { - id: "claude-routines".to_string(), - title: "Daily Routines".to_string(), - window: window(95.0), - fallback_lane: false, - icon_fallback: None, - }]; - - let baseline = crate::commands::ProviderUsagePresentationSnapshot::new( - raw.clone(), - &Settings::default(), - ); - assert_eq!(baseline.selected_metric.used_percent, 95.0); - - let mut settings = Settings::default(); - settings.set_hidden_usage_item_ids( - ProviderId::Claude, - vec![codexbar::settings::CLAUDE_DAILY_ROUTINES_USAGE_ITEM_ID.to_string()], - ); - let hidden = - crate::commands::ProviderUsagePresentationSnapshot::new(raw.clone(), &settings); - - assert_eq!(hidden.selected_metric.used_percent, 95.0); - assert_eq!(hidden.snapshot.extra_rate_windows[0].id, "claude-routines"); - assert_eq!( - hidden.snapshot.extra_rate_windows[0].window.used_percent, - 95.0 - ); - - settings.set_hidden_usage_item_ids(ProviderId::Claude, Vec::new()); - let restored = crate::commands::ProviderUsagePresentationSnapshot::new(raw, &settings); - - assert_eq!(restored.selected_metric.used_percent, 95.0); - assert_eq!( - restored.snapshot.extra_rate_windows[0].id, - "claude-routines" - ); - assert!(restored.hidden_usage_item_ids.is_empty()); - } - - fn litellm_budgets(personal: f64, team: Option) -> ProviderUsageSnapshot { - let mut snapshot = snapshot(); - snapshot.provider_id = "litellm".to_string(); - snapshot.primary = window(personal); - snapshot.secondary = team.map(window); - snapshot - } - - const AGENT_RESET: &str = "2026-08-16T05:00:00Z"; - - fn agent_window(used_percent: f64, minutes: u32) -> RateWindowSnapshot { - RateWindowSnapshot { - window_minutes: Some(minutes), - resets_at: Some(AGENT_RESET.to_string()), - ..window(used_percent) - } - } - - fn agent_extra( - id: &str, - window: RateWindowSnapshot, - icon_fallback: Option, - ) -> crate::commands::NamedRateWindowSnapshot { - crate::commands::NamedRateWindowSnapshot { - id: id.to_string(), - title: id.to_string(), - window, - fallback_lane: false, - icon_fallback, - } - } - - /// Bridge shape of a Doubao Coding Plan snapshot that also reports Agent - /// Plan lanes (`doubao-agent-*`), with optional Coding Plan session/weekly. - fn doubao_snapshot( - coding_session: Option, - coding_weekly: Option, - ) -> ProviderUsageSnapshot { - let mut snapshot = snapshot(); - snapshot.provider_id = "doubao".to_string(); - snapshot.primary = coding_session.map_or_else( - || RateWindowSnapshot { - window_minutes: Some(300), - is_informational: true, - ..window(0.0) - }, - window, - ); - snapshot.secondary = coding_weekly.map(window); - snapshot.extra_rate_windows = vec![ - agent_extra( - "doubao-agent-session", - agent_window(0.0, 300), - Some(IconLane::Primary), - ), - agent_extra( - "doubao-agent-weekly", - agent_window(31.0, 10_080), - Some(IconLane::Secondary), - ), - agent_extra("doubao-agent-monthly", agent_window(72.0, 43_200), None), - ]; - snapshot - } - - #[test] - fn litellm_automatic_prefers_the_team_budget_over_a_fuller_personal_budget() { - let snapshot = litellm_budgets(60.0, Some(7.0)); - - assert_eq!( - selected_usage_window(&snapshot, &Settings::default()).used_percent, - 7.0 - ); - let (selected, companion) = selected_usage_icon_windows(&snapshot, &Settings::default()); - assert_eq!(selected.used_percent, 7.0); - assert_eq!(companion.map(|window| window.used_percent), Some(60.0)); - } - - #[test] - fn litellm_automatic_shows_an_exhausted_budget_first() { - let personal_exhausted = litellm_budgets(100.0, Some(7.0)); - assert_eq!( - selected_usage_window(&personal_exhausted, &Settings::default()).used_percent, - 100.0 - ); - - let team_exhausted = litellm_budgets(40.0, Some(100.0)); - assert_eq!( - selected_usage_window(&team_exhausted, &Settings::default()).used_percent, - 100.0 - ); - } - - #[test] - fn litellm_automatic_uses_the_only_budget_and_explicit_choices_still_win() { - let personal_only = litellm_budgets(25.0, None); - assert_eq!( - selected_usage_window(&personal_only, &Settings::default()).used_percent, - 25.0 - ); - - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::LiteLLM, MetricPreference::Session); - assert_eq!( - selected_usage_window(&litellm_budgets(60.0, Some(7.0)), &settings).used_percent, - 60.0 - ); - } - - #[test] - fn doubao_icon_windows_fall_back_to_agent_plan_lanes() { - let mut settings = Settings::default(); - settings.set_provider_metric(ProviderId::Doubao, MetricPreference::Session); - - for (has_session, has_weekly) in - [(false, false), (false, true), (true, false), (true, true)] - { - let snapshot = doubao_snapshot(has_session.then_some(10.0), has_weekly.then_some(20.0)); - let (primary, secondary) = selected_usage_icon_windows(&snapshot, &settings); - let secondary = secondary.expect("secondary icon lane"); - - assert_eq!(primary.used_percent, if has_session { 10.0 } else { 0.0 }); - assert_eq!(secondary.used_percent, if has_weekly { 20.0 } else { 31.0 }); - assert!(!primary.is_informational); - assert_eq!( - primary.window_minutes, - if has_session { None } else { Some(300) } - ); - assert_eq!( - secondary.window_minutes, - if has_weekly { None } else { Some(10_080) } - ); - assert_eq!( - primary.resets_at.as_deref(), - (!has_session).then_some(AGENT_RESET) - ); - assert_eq!( - secondary.resets_at.as_deref(), - (!has_weekly).then_some(AGENT_RESET) - ); - } - } - - #[test] - fn doubao_icon_fallback_does_not_change_the_reported_snapshot() { - let raw = doubao_snapshot(None, None); - let presentation = crate::commands::ProviderUsagePresentationSnapshot::new( - raw.clone(), - &Settings::default(), - ); - - assert!(presentation.snapshot.primary.is_informational); - assert!(presentation.snapshot.secondary.is_none()); - assert!(presentation.snapshot.tertiary.is_none()); - assert_eq!( - presentation - .snapshot - .extra_rate_windows - .iter() - .map(|extra| extra.id.as_str()) - .collect::>(), - [ - "doubao-agent-session", - "doubao-agent-weekly", - "doubao-agent-monthly" - ] - ); - assert!(!presentation.selected_metric.is_informational); - } - - #[test] - fn doubao_icon_windows_preserve_missing_lanes() { - let no_hints = |levels: &[(&str, f64, u32)]| { - let mut snapshot = doubao_snapshot(None, None); - snapshot.extra_rate_windows = levels - .iter() - .map(|(id, percent, minutes)| { - agent_extra(id, agent_window(*percent, *minutes), None) - }) - .collect(); - snapshot - }; - // Monthly and team buckets never stand in for the session/weekly lanes. - for id in ["doubao-agent-monthly", "doubao-agent-team-session"] { - let snapshot = no_hints(&[(id, 25.0, 300)]); - let resolved = with_icon_fallbacks(&snapshot); - assert!(matches!(resolved, Cow::Borrowed(_)), "{id}"); - assert!(resolved.primary.is_informational, "{id}"); - assert!(resolved.secondary.is_none(), "{id}"); - } - - // Agent weekly alone fills only the weekly lane; no session is invented. - let mut snapshot = doubao_snapshot(None, None); - snapshot - .extra_rate_windows - .retain(|extra| extra.id == "doubao-agent-weekly"); - let resolved = with_icon_fallbacks(&snapshot); - assert!(resolved.primary.is_informational); - assert_eq!(resolved.secondary.as_ref().unwrap().used_percent, 31.0); - - // Coding session alone (agent lanes absent): unchanged, nothing invented. - let mut snapshot = doubao_snapshot(Some(25.0), None); - snapshot.extra_rate_windows.clear(); - let resolved = with_icon_fallbacks(&snapshot); - assert!(matches!(resolved, Cow::Borrowed(_))); - let (primary, companion) = selected_usage_icon_windows(&snapshot, &Settings::default()); - assert_eq!(primary.used_percent, 25.0); - assert!(companion.is_none()); - } - - #[test] - fn doubao_automatic_icon_uses_agent_lanes_not_team_windows() { - let mut snapshot = doubao_snapshot(None, None); - snapshot.extra_rate_windows = vec![ - agent_extra( - "doubao-agent-session", - agent_window(42.0, 300), - Some(IconLane::Primary), - ), - agent_extra( - "doubao-agent-weekly", - agent_window(67.0, 10_080), - Some(IconLane::Secondary), - ), - agent_extra("doubao-agent-team-session", agent_window(91.0, 300), None), - agent_extra("doubao-agent-team-weekly", agent_window(88.0, 10_080), None), - ]; - let settings = Settings::default(); - - let (top, bottom) = selected_usage_icon_windows(&snapshot, &settings); - assert_eq!(top.used_percent, 42.0); - assert_eq!(bottom.expect("bottom lane").used_percent, 67.0); - assert_eq!( - selected_usage_window(&snapshot, &settings).used_percent, - 67.0 - ); - assert_eq!( - crate::tray_presentation::headline_window(&snapshot).used_percent, - 42.0 - ); - } - - #[test] - fn icon_fallback_hints_are_ignored_while_the_core_lane_is_present() { - let snapshot = doubao_snapshot(Some(10.0), Some(20.0)); - let resolved = with_icon_fallbacks(&snapshot); - - assert!(matches!(resolved, Cow::Borrowed(_))); - assert_eq!(resolved.primary.used_percent, 10.0); - assert_eq!(resolved.secondary.as_ref().unwrap().used_percent, 20.0); - } -} +mod tests; diff --git a/apps/desktop-tauri/src-tauri/src/usage_metric/tests.rs b/apps/desktop-tauri/src-tauri/src/usage_metric/tests.rs new file mode 100644 index 00000000000..40dad7d84ac --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/usage_metric/tests.rs @@ -0,0 +1,739 @@ +use super::*; +use codexbar::providers::copilot::SEAT_CREDIT_WINDOW_ID; + +fn window(used_percent: f64) -> RateWindowSnapshot { + derived_window(used_percent, None) +} + +fn snapshot() -> ProviderUsageSnapshot { + ProviderUsageSnapshot { + provider_id: "codex".to_string(), + display_name: "Codex".to_string(), + primary: window(20.0), + secondary: Some(window(60.0)), + source_label: "test".to_string(), + updated_at: "2026-08-16T00:00:00Z".to_string(), + ..Default::default() + } +} + +#[test] +fn weekly_preference_selects_the_weekly_window() { + let snapshot = snapshot(); + let mut settings = Settings::default(); + settings.set_provider_metric(ProviderId::Codex, MetricPreference::Weekly); + + assert_eq!( + selected_usage_window(&snapshot, &settings).used_percent, + 60.0 + ); +} + +#[test] +fn missing_selected_session_falls_back_to_a_real_window() { + let mut snapshot = snapshot(); + snapshot.primary.is_informational = true; + snapshot.primary.used_percent = 0.0; + let mut settings = Settings::default(); + settings.set_provider_metric(ProviderId::Codex, MetricPreference::Session); + + assert_eq!( + selected_usage_window(&snapshot, &settings).used_percent, + 60.0 + ); +} + +#[test] +fn automatic_selects_the_highest_real_window() { + let snapshot = snapshot(); + + assert_eq!( + selected_usage_window(&snapshot, &Settings::default()).used_percent, + 60.0 + ); +} + +#[test] +fn copilot_automatic_uses_seat_credit_progress_without_metered_quota() { + let mut snapshot = snapshot(); + snapshot.provider_id = "copilot".to_string(); + snapshot.primary = RateWindowSnapshot { + is_informational: true, + ..window(0.0) + }; + snapshot.secondary = None; + snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { + id: SEAT_CREDIT_WINDOW_ID.to_string(), + title: "Credits used".to_string(), + window: window(35.0), + fallback_lane: true, + icon_fallback: None, + }]; + + assert_eq!( + selected_usage_window(&snapshot, &Settings::default()).used_percent, + 35.0 + ); +} + +#[test] +fn copilot_automatic_keeps_metered_quota_authoritative_over_seat_credits() { + let mut snapshot = snapshot(); + snapshot.provider_id = "copilot".to_string(); + snapshot.primary = window(20.0); + snapshot.secondary = None; + snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { + id: SEAT_CREDIT_WINDOW_ID.to_string(), + title: "Credits used".to_string(), + window: window(90.0), + fallback_lane: true, + icon_fallback: None, + }]; + + assert_eq!( + selected_usage_window(&snapshot, &Settings::default()).used_percent, + 20.0 + ); +} + +#[test] +fn copilot_explicit_session_does_not_fall_back_to_seat_credits() { + let mut snapshot = snapshot(); + snapshot.provider_id = "copilot".to_string(); + snapshot.primary = RateWindowSnapshot { + is_informational: true, + ..window(0.0) + }; + snapshot.secondary = None; + snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { + id: SEAT_CREDIT_WINDOW_ID.to_string(), + title: "Credits used".to_string(), + window: window(35.0), + fallback_lane: true, + icon_fallback: None, + }]; + let mut settings = Settings::default(); + let provider = ProviderId::from_cli_name(&snapshot.provider_id).expect("copilot provider"); + settings.set_provider_metric(provider, MetricPreference::Session); + + let selected = selected_usage_window(&snapshot, &settings); + assert!(selected.is_informational); + assert_eq!(selected.used_percent, 0.0); +} + +#[test] +fn cursor_automatic_uses_semantic_monthly_lane_and_keeps_grok_bot_explicit() { + let mut snapshot = snapshot(); + snapshot.provider_id = "cursor".to_string(); + snapshot.primary = window(85.0); + snapshot.secondary = Some(window(20.0)); + snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { + id: "cursor-grok-bot".to_string(), + title: "Grok Bot".to_string(), + window: window(95.0), + fallback_lane: false, + icon_fallback: None, + }]; + + assert_eq!( + selected_usage_window(&snapshot, &Settings::default()).used_percent, + 20.0 + ); + + let mut settings = Settings::default(); + settings.set_provider_metric(ProviderId::Cursor, MetricPreference::ExtraUsage); + assert_eq!( + selected_usage_window(&snapshot, &settings).used_percent, + 95.0 + ); +} + +#[test] +fn kimi_monthly_only_snapshot_selects_the_total_usage_lane() { + // Upstream 0.60.5 #3694: a Code API response may report only the + // monthly Total usage pool. The weekly lane is then an informational + // placeholder, and every metric preference lands on the monthly lane. + let mut snapshot = snapshot(); + snapshot.provider_id = "kimi".to_string(); + snapshot.primary = RateWindowSnapshot { + is_informational: true, + ..window(0.0) + }; + snapshot.secondary = None; + snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { + id: "kimi-monthly".to_string(), + title: "Total usage".to_string(), + window: window(100.0), + fallback_lane: false, + icon_fallback: None, + }]; + + for preference in [ + MetricPreference::Automatic, + MetricPreference::Session, + MetricPreference::Weekly, + ] { + let mut settings = Settings::default(); + settings.set_provider_metric(ProviderId::Kimi, preference); + let selected = selected_usage_window(&snapshot, &settings); + assert!(!selected.is_informational, "{preference:?}"); + assert_eq!(selected.used_percent, 100.0, "{preference:?}"); + } +} + +#[test] +fn opencodego_automatic_prefers_explicitly_exhausted_window_over_higher_percentage() { + let mut snapshot = snapshot(); + snapshot.provider_id = "opencodego".to_string(); + snapshot.primary.is_exhausted = true; + + let selected = selected_usage_window(&snapshot, &Settings::default()); + + assert_eq!(selected.used_percent, 20.0); + assert!(selected.is_exhausted); +} + +#[test] +fn claude_and_codex_automatic_keep_highest_used_window() { + for provider_id in ["claude", "codex"] { + let mut snapshot = snapshot(); + snapshot.provider_id = provider_id.to_string(); + snapshot.primary.is_exhausted = true; + + let selected = selected_usage_window(&snapshot, &Settings::default()); + + assert_eq!( + selected.used_percent, 60.0, + "{provider_id} should keep highest-used automatic selection" + ); + assert!(!selected.is_exhausted); + } +} + +#[test] +fn automatic_treats_a_full_window_as_exhausted_even_without_the_flag() { + let mut snapshot = snapshot(); + let mut full = window(100.0); + full.is_exhausted = false; + snapshot.tertiary = Some(full); + + let selected = selected_usage_window(&snapshot, &Settings::default()); + + assert_eq!(selected.used_percent, 100.0); + assert!(!selected.is_exhausted); +} + +#[test] +fn non_automatic_highest_window_keeps_percentage_order() { + let healthy = window(80.0); + let mut exhausted = window(20.0); + exhausted.is_exhausted = true; + + let selected = highest_window([&healthy, &exhausted].into_iter()).expect("window"); + + assert_eq!(selected.used_percent, 80.0); +} + +#[test] +fn antigravity_automatic_prefers_active_core_quota_over_exhausted_extra_window() { + let mut snapshot = snapshot(); + snapshot.provider_id = "antigravity".to_string(); + snapshot.primary = window(100.0); + snapshot.primary.is_exhausted = true; + snapshot.primary_label = Some("Gemini 5h".to_string()); + snapshot.secondary = Some(window(88.0)); + snapshot.secondary_label = Some("Gemini Weekly".to_string()); + snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { + id: "antigravity-quota-summary-3p-weekly".to_string(), + title: "Claude/GPT weekly".to_string(), + window: window(100.0), + fallback_lane: false, + icon_fallback: None, + }]; + + let selected = selected_usage_window(&snapshot, &Settings::default()); + + assert_eq!(selected.used_percent, 88.0); + assert!(!selected.is_exhausted); +} + +#[test] +fn antigravity_automatic_uses_core_slots_only() { + let mut snapshot = snapshot(); + snapshot.provider_id = "antigravity".to_string(); + snapshot.primary = window(80.0); + snapshot.secondary = Some(window(20.0)); + snapshot.model_specific = Some(window(90.0)); + snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { + id: "legacy-other".to_string(), + title: "Other".to_string(), + window: window(100.0), + fallback_lane: false, + icon_fallback: None, + }]; + + let selected = selected_usage_window(&snapshot, &Settings::default()); + + assert_eq!(selected.used_percent, 90.0); +} + +#[test] +fn single_meaningful_quota_omits_the_companion_icon_lane() { + let mut snapshot = snapshot(); + snapshot + .secondary + .as_mut() + .expect("fixture has a secondary window") + .is_informational = true; + + let (selected, companion) = selected_usage_icon_windows(&snapshot, &Settings::default()); + + assert_eq!(selected.used_percent, 20.0); + assert!(companion.is_none()); +} + +#[test] +fn average_preference_derives_the_combined_percentage() { + let mut snapshot = snapshot(); + snapshot.provider_id = "gemini".to_string(); + let mut settings = Settings::default(); + settings.set_provider_metric(ProviderId::Gemini, MetricPreference::Average); + + let selected = selected_usage_window(&snapshot, &settings); + assert_eq!(selected.used_percent, 40.0); + assert_eq!(selected.remaining_percent, 60.0); +} + +fn mistral_snapshot(plan: Option) -> ProviderUsageSnapshot { + let mut snapshot = snapshot(); + snapshot.provider_id = "mistral".to_string(); + snapshot.display_name = "Mistral".to_string(); + snapshot.primary = window(2.0); + snapshot.primary_label = Some("Included API".to_string()); + snapshot.secondary = None; + snapshot.extra_rate_windows = plan + .map(|window| crate::commands::NamedRateWindowSnapshot { + id: codexbar::providers::mistral::MONTHLY_PLAN_WINDOW_ID.to_string(), + title: "Monthly Plan".to_string(), + window, + fallback_lane: false, + icon_fallback: None, + }) + .into_iter() + .collect(); + snapshot +} + +fn metric_settings(provider: ProviderId, preference: MetricPreference) -> Settings { + let mut settings = Settings::default(); + settings.set_provider_metric(provider, preference); + settings +} + +#[test] +fn mistral_monthly_plan_selects_the_vibe_plan_window() { + let mut snapshot = mistral_snapshot(Some(window(42.0))); + snapshot.primary = window(80.0); + + let monthly_plan = metric_settings(ProviderId::Mistral, MetricPreference::MonthlyPlan); + assert_eq!( + selected_usage_window(&snapshot, &monthly_plan).used_percent, + 42.0 + ); + // Included API and Automatic stay separate choices (upstream #4072). + let included_api = metric_settings(ProviderId::Mistral, MetricPreference::Session); + assert_eq!( + selected_usage_window(&snapshot, &included_api).used_percent, + 80.0 + ); + assert_eq!( + selected_usage_window(&snapshot, &Settings::default()).used_percent, + 80.0 + ); + let presentation = + crate::commands::ProviderUsagePresentationSnapshot::new(snapshot, &monthly_plan); + assert_eq!(presentation.selected_metric.used_percent, 42.0); +} + +#[test] +fn mistral_monthly_plan_falls_back_to_included_api_without_a_known_plan() { + let settings = metric_settings(ProviderId::Mistral, MetricPreference::MonthlyPlan); + let unknown_plan = RateWindowSnapshot { + is_informational: true, + ..window(0.0) + }; + + for snapshot in [mistral_snapshot(None), mistral_snapshot(Some(unknown_plan))] { + let selected = selected_usage_window(&snapshot, &settings); + assert_eq!(selected.used_percent, 2.0); + assert!(!selected.is_informational); + } +} + +#[test] +fn mistral_monthly_plan_never_selects_a_spend_window() { + let mut snapshot = mistral_snapshot(None); + snapshot.cost = Some(crate::commands::CostSnapshotBridge { + used: 45.0, + limit: Some(50.0), + remaining: Some(5.0), + currency_code: "EUR".to_string(), + currency_symbol: Some("€".to_string()), + period: "month".to_string(), + resets_at: None, + formatted_used: "€45.00".to_string(), + formatted_limit: Some("€50.00".to_string()), + balance: None, + balance_updated_at: None, + account_id: None, + formatted_balance: None, + daily: Vec::new(), + always_visible: false, + }); + let settings = metric_settings(ProviderId::Mistral, MetricPreference::MonthlyPlan); + + assert_eq!( + selected_usage_window(&snapshot, &settings).used_percent, + 2.0 + ); +} + +#[test] +fn monthly_plan_without_a_provider_plan_window_falls_through_to_automatic() { + let mut snapshot = snapshot(); + snapshot.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { + id: codexbar::providers::mistral::MONTHLY_PLAN_WINDOW_ID.to_string(), + title: "Monthly Plan".to_string(), + window: window(90.0), + fallback_lane: false, + icon_fallback: None, + }]; + let settings = metric_settings(ProviderId::Codex, MetricPreference::MonthlyPlan); + + // Codex declares no plan window, so a same-named lane is not special + // and the choice resolves like Automatic (highest real window). + assert_eq!( + selected_usage_window(&snapshot, &settings).used_percent, + selected_usage_window(&snapshot, &Settings::default()).used_percent + ); + assert!(monthly_plan_window(&snapshot, Some(ProviderId::Codex)).is_none()); +} + +#[test] +fn presentation_payload_flattens_the_snapshot_and_selected_metric() { + let presentation = + crate::commands::ProviderUsagePresentationSnapshot::new(snapshot(), &Settings::default()); + let value = serde_json::to_value(presentation).expect("serialize presentation"); + + assert_eq!(value["providerId"], "codex"); + assert_eq!(value["selectedMetric"]["usedPercent"], 60.0); + assert!(value.get("snapshot").is_none()); +} + +#[test] +fn hidden_usage_items_are_presentation_metadata_and_do_not_change_selected_metric() { + let mut settings = Settings::default(); + settings.set_hidden_usage_item_ids(ProviderId::Codex, vec!["metric:secondary".to_string()]); + + let presentation = + crate::commands::ProviderUsagePresentationSnapshot::new(snapshot(), &settings); + + assert!(presentation.snapshot.secondary.is_some()); + assert_eq!(presentation.selected_metric.used_percent, 60.0); + assert_eq!( + presentation.hidden_usage_item_ids, + vec!["metric:secondary".to_string()] + ); + let value = serde_json::to_value(presentation).expect("serialize presentation"); + assert_eq!( + value["hiddenUsageItemIds"], + serde_json::json!(["metric:secondary"]) + ); +} + +#[test] +fn claude_routines_hide_and_restore_preserve_raw_data_and_selected_metric() { + let mut raw = snapshot(); + raw.provider_id = "claude".to_string(); + raw.extra_rate_windows = vec![crate::commands::NamedRateWindowSnapshot { + id: "claude-routines".to_string(), + title: "Daily Routines".to_string(), + window: window(95.0), + fallback_lane: false, + icon_fallback: None, + }]; + + let baseline = + crate::commands::ProviderUsagePresentationSnapshot::new(raw.clone(), &Settings::default()); + assert_eq!(baseline.selected_metric.used_percent, 95.0); + + let mut settings = Settings::default(); + settings.set_hidden_usage_item_ids( + ProviderId::Claude, + vec![codexbar::settings::CLAUDE_DAILY_ROUTINES_USAGE_ITEM_ID.to_string()], + ); + let hidden = crate::commands::ProviderUsagePresentationSnapshot::new(raw.clone(), &settings); + + assert_eq!(hidden.selected_metric.used_percent, 95.0); + assert_eq!(hidden.snapshot.extra_rate_windows[0].id, "claude-routines"); + assert_eq!( + hidden.snapshot.extra_rate_windows[0].window.used_percent, + 95.0 + ); + + settings.set_hidden_usage_item_ids(ProviderId::Claude, Vec::new()); + let restored = crate::commands::ProviderUsagePresentationSnapshot::new(raw, &settings); + + assert_eq!(restored.selected_metric.used_percent, 95.0); + assert_eq!( + restored.snapshot.extra_rate_windows[0].id, + "claude-routines" + ); + assert!(restored.hidden_usage_item_ids.is_empty()); +} + +fn litellm_budgets(personal: f64, team: Option) -> ProviderUsageSnapshot { + let mut snapshot = snapshot(); + snapshot.provider_id = "litellm".to_string(); + snapshot.primary = window(personal); + snapshot.secondary = team.map(window); + snapshot +} + +const AGENT_RESET: &str = "2026-08-16T05:00:00Z"; + +fn agent_window(used_percent: f64, minutes: u32) -> RateWindowSnapshot { + RateWindowSnapshot { + window_minutes: Some(minutes), + resets_at: Some(AGENT_RESET.to_string()), + ..window(used_percent) + } +} + +fn agent_extra( + id: &str, + window: RateWindowSnapshot, + icon_fallback: Option, +) -> crate::commands::NamedRateWindowSnapshot { + crate::commands::NamedRateWindowSnapshot { + id: id.to_string(), + title: id.to_string(), + window, + fallback_lane: false, + icon_fallback, + } +} + +/// Bridge shape of a Doubao Coding Plan snapshot that also reports Agent +/// Plan lanes (`doubao-agent-*`), with optional Coding Plan session/weekly. +fn doubao_snapshot( + coding_session: Option, + coding_weekly: Option, +) -> ProviderUsageSnapshot { + let mut snapshot = snapshot(); + snapshot.provider_id = "doubao".to_string(); + snapshot.primary = coding_session.map_or_else( + || RateWindowSnapshot { + window_minutes: Some(300), + is_informational: true, + ..window(0.0) + }, + window, + ); + snapshot.secondary = coding_weekly.map(window); + snapshot.extra_rate_windows = vec![ + agent_extra( + "doubao-agent-session", + agent_window(0.0, 300), + Some(IconLane::Primary), + ), + agent_extra( + "doubao-agent-weekly", + agent_window(31.0, 10_080), + Some(IconLane::Secondary), + ), + agent_extra("doubao-agent-monthly", agent_window(72.0, 43_200), None), + ]; + snapshot +} + +#[test] +fn litellm_automatic_prefers_the_team_budget_over_a_fuller_personal_budget() { + let snapshot = litellm_budgets(60.0, Some(7.0)); + + assert_eq!( + selected_usage_window(&snapshot, &Settings::default()).used_percent, + 7.0 + ); + let (selected, companion) = selected_usage_icon_windows(&snapshot, &Settings::default()); + assert_eq!(selected.used_percent, 7.0); + assert_eq!(companion.map(|window| window.used_percent), Some(60.0)); +} + +#[test] +fn litellm_automatic_shows_an_exhausted_budget_first() { + let personal_exhausted = litellm_budgets(100.0, Some(7.0)); + assert_eq!( + selected_usage_window(&personal_exhausted, &Settings::default()).used_percent, + 100.0 + ); + + let team_exhausted = litellm_budgets(40.0, Some(100.0)); + assert_eq!( + selected_usage_window(&team_exhausted, &Settings::default()).used_percent, + 100.0 + ); +} + +#[test] +fn litellm_automatic_uses_the_only_budget_and_explicit_choices_still_win() { + let personal_only = litellm_budgets(25.0, None); + assert_eq!( + selected_usage_window(&personal_only, &Settings::default()).used_percent, + 25.0 + ); + + let mut settings = Settings::default(); + settings.set_provider_metric(ProviderId::LiteLLM, MetricPreference::Session); + assert_eq!( + selected_usage_window(&litellm_budgets(60.0, Some(7.0)), &settings).used_percent, + 60.0 + ); +} + +#[test] +fn doubao_icon_windows_fall_back_to_agent_plan_lanes() { + let mut settings = Settings::default(); + settings.set_provider_metric(ProviderId::Doubao, MetricPreference::Session); + + for (has_session, has_weekly) in [(false, false), (false, true), (true, false), (true, true)] { + let snapshot = doubao_snapshot(has_session.then_some(10.0), has_weekly.then_some(20.0)); + let (primary, secondary) = selected_usage_icon_windows(&snapshot, &settings); + let secondary = secondary.expect("secondary icon lane"); + + assert_eq!(primary.used_percent, if has_session { 10.0 } else { 0.0 }); + assert_eq!(secondary.used_percent, if has_weekly { 20.0 } else { 31.0 }); + assert!(!primary.is_informational); + assert_eq!( + primary.window_minutes, + if has_session { None } else { Some(300) } + ); + assert_eq!( + secondary.window_minutes, + if has_weekly { None } else { Some(10_080) } + ); + assert_eq!( + primary.resets_at.as_deref(), + (!has_session).then_some(AGENT_RESET) + ); + assert_eq!( + secondary.resets_at.as_deref(), + (!has_weekly).then_some(AGENT_RESET) + ); + } +} + +#[test] +fn doubao_icon_fallback_does_not_change_the_reported_snapshot() { + let raw = doubao_snapshot(None, None); + let presentation = + crate::commands::ProviderUsagePresentationSnapshot::new(raw.clone(), &Settings::default()); + + assert!(presentation.snapshot.primary.is_informational); + assert!(presentation.snapshot.secondary.is_none()); + assert!(presentation.snapshot.tertiary.is_none()); + assert_eq!( + presentation + .snapshot + .extra_rate_windows + .iter() + .map(|extra| extra.id.as_str()) + .collect::>(), + [ + "doubao-agent-session", + "doubao-agent-weekly", + "doubao-agent-monthly" + ] + ); + assert!(!presentation.selected_metric.is_informational); +} + +#[test] +fn doubao_icon_windows_preserve_missing_lanes() { + let no_hints = |levels: &[(&str, f64, u32)]| { + let mut snapshot = doubao_snapshot(None, None); + snapshot.extra_rate_windows = levels + .iter() + .map(|(id, percent, minutes)| agent_extra(id, agent_window(*percent, *minutes), None)) + .collect(); + snapshot + }; + // Monthly and team buckets never stand in for the session/weekly lanes. + for id in ["doubao-agent-monthly", "doubao-agent-team-session"] { + let snapshot = no_hints(&[(id, 25.0, 300)]); + let resolved = with_icon_fallbacks(&snapshot); + assert!(matches!(resolved, Cow::Borrowed(_)), "{id}"); + assert!(resolved.primary.is_informational, "{id}"); + assert!(resolved.secondary.is_none(), "{id}"); + } + + // Agent weekly alone fills only the weekly lane; no session is invented. + let mut snapshot = doubao_snapshot(None, None); + snapshot + .extra_rate_windows + .retain(|extra| extra.id == "doubao-agent-weekly"); + let resolved = with_icon_fallbacks(&snapshot); + assert!(resolved.primary.is_informational); + assert_eq!(resolved.secondary.as_ref().unwrap().used_percent, 31.0); + + // Coding session alone (agent lanes absent): unchanged, nothing invented. + let mut snapshot = doubao_snapshot(Some(25.0), None); + snapshot.extra_rate_windows.clear(); + let resolved = with_icon_fallbacks(&snapshot); + assert!(matches!(resolved, Cow::Borrowed(_))); + let (primary, companion) = selected_usage_icon_windows(&snapshot, &Settings::default()); + assert_eq!(primary.used_percent, 25.0); + assert!(companion.is_none()); +} + +#[test] +fn doubao_automatic_icon_uses_agent_lanes_not_team_windows() { + let mut snapshot = doubao_snapshot(None, None); + snapshot.extra_rate_windows = vec![ + agent_extra( + "doubao-agent-session", + agent_window(42.0, 300), + Some(IconLane::Primary), + ), + agent_extra( + "doubao-agent-weekly", + agent_window(67.0, 10_080), + Some(IconLane::Secondary), + ), + agent_extra("doubao-agent-team-session", agent_window(91.0, 300), None), + agent_extra("doubao-agent-team-weekly", agent_window(88.0, 10_080), None), + ]; + let settings = Settings::default(); + + let (top, bottom) = selected_usage_icon_windows(&snapshot, &settings); + assert_eq!(top.used_percent, 42.0); + assert_eq!(bottom.expect("bottom lane").used_percent, 67.0); + assert_eq!( + selected_usage_window(&snapshot, &settings).used_percent, + 67.0 + ); + assert_eq!( + crate::tray_presentation::headline_window(&snapshot).used_percent, + 42.0 + ); +} + +#[test] +fn icon_fallback_hints_are_ignored_while_the_core_lane_is_present() { + let snapshot = doubao_snapshot(Some(10.0), Some(20.0)); + let resolved = with_icon_fallbacks(&snapshot); + + assert!(matches!(resolved, Cow::Borrowed(_))); + assert_eq!(resolved.primary.used_percent, 10.0); + assert_eq!(resolved.secondary.as_ref().unwrap().used_percent, 20.0); +} From d760e8e273b27ad28ecc0eb1a689b4bf15481318 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:26:54 +0700 Subject: [PATCH 15/22] Split fetch-context building and notifications out of commands/providers.rs --- .../src-tauri/src/commands/providers.rs | 589 +----------------- .../src/commands/providers/fetch_context.rs | 338 ++++++++++ .../src/commands/providers/notify.rs | 243 ++++++++ 3 files changed, 589 insertions(+), 581 deletions(-) create mode 100644 apps/desktop-tauri/src-tauri/src/commands/providers/fetch_context.rs create mode 100644 apps/desktop-tauri/src-tauri/src/commands/providers/notify.rs diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers.rs b/apps/desktop-tauri/src-tauri/src/commands/providers.rs index 97acacca875..23449c83886 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/providers.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/providers.rs @@ -11,8 +11,16 @@ use codexbar::notifications::WarningScope; use serde::Serialize; use std::sync::Arc; +mod fetch_context; +mod notify; mod reset_backfill; +#[cfg(test)] +pub(crate) use fetch_context::provider_cookie_domain; +pub(crate) use fetch_context::{build_fetch_context, provider_fetch_timeout}; +pub(super) use notify::quota_notification_account_identity; +use notify::update_tray_and_notifications; + const MAX_CONCURRENT_PROVIDER_FETCHES: usize = 8; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -58,345 +66,6 @@ pub(crate) fn invalidate_account_usage( pending } -// ── Provider refresh commands ──────────────────────────────────────── - -/// Build a `FetchContext` for a provider using persisted cookies/keys. -pub(crate) fn build_fetch_context( - id: ProviderId, - settings: &Settings, - cookies: &ManualCookies, - api_keys: &ApiKeys, - token_accounts: &HashMap, -) -> FetchContext { - let provider = instantiate_provider(id); - let cookie_source = settings.cookie_source(id); - let stored_cookie = cookies.get(id.cli_name()).map(|s| s.to_string()); - let stored_api_key = api_keys.get(id.cli_name()).map(|s| s.to_string()); - let token_override = token_accounts - .get(&id) - .and_then(|data| data.active_account()) - .cloned() - .map(|account| TokenAccountOverride::from_account(id, account)); - let active_token_cookie = token_override - .as_ref() - .and_then(|override_data| override_data.cookie_header.clone()); - let defer_provider_browser_cookie_lookup = provider.owns_browser_cookie_resolution() - && active_token_cookie.is_none() - && stored_cookie.is_none(); - let active_token_env = token_override - .as_ref() - .and_then(|override_data| override_data.env_override.as_ref()); - let active_token_api_key = active_token_env.and_then(|env| env.values().next().cloned()); - let usage_source = SourceMode::parse(settings.usage_source(id)).unwrap_or_default(); - let token_account_kind = token_override.as_ref().map(|account| account.kind); - // Selected token-account key overrides a stored provider apiKey (upstream #2271 / #1183). - let api_key = active_token_api_key.clone().or(stored_api_key); - let has_kimi_code_api_key = - id == ProviderId::Kimi && api_key.as_deref().is_some_and(|key| !key.trim().is_empty()); - let has_opencodego_api_key = id == ProviderId::OpenCodeGo - && api_key.as_deref().is_some_and(|key| !key.trim().is_empty()); - - // Providers whose cookies only enrich an API result keep the configured - // usage source. Off and the default manual-without-cookie state never read - // a browser; only Automatic imports one (`browser_cookie_import`). - let cookies_only_enrich_usage = provider.cookies_only_enrich_usage(); - let browser_cookie_import = - cookies_only_enrich_usage && matches!(cookie_source, "auto" | "browser" | "web"); - // #433: an explicitly selected, non-empty Claude manual cookie is - // authoritative. Do not let an active OAuth token account silently - // replace it; this keeps tray refresh behavior aligned with diagnose, - // whose Claude Auto path tries the supplied Web cookie before OAuth. - let manual_cookie_wins = cookie_source == "manual" - && provider.manual_cookie_precedes_token_account() - && stored_cookie - .as_deref() - .is_some_and(|cookie| !cookie.trim().is_empty()); - let (mut source_mode, mut cookie_header, fails_closed_without_cookie) = if id - .cookie_domain() - .is_none() - { - // The #725 wallet rule (#725 keeps Auto for wallet providers) folds into - // the account-source decision; other no-cookie providers still honor - // cookie_source below (the account-source pack's fix for the #619 merge). - // A wallet provider keeps its saved usage source even with a selected - // token account: rewriting it to OAuth would skip the wallet read - // (#725 / upstream base-source resolver). - let keeps_auto = - usage_source == SourceMode::Auto && provider.token_account_preserves_auto_source(); - if active_token_env.is_some() && keeps_auto { - (usage_source, None, false) - } else if active_token_env.is_some() - && provider.web_is_opt_in() - && usage_source != SourceMode::Web - { - // Opt-in web providers keep their default credential lane - // unless the usage source is explicitly Web. - (usage_source, None, false) - } else if manual_cookie_wins { - (SourceMode::Web, stored_cookie.clone(), false) - } else if active_token_env.is_some() { - (SourceMode::OAuth, None, false) - } else { - (usage_source, None, false) - } - } else if cookies_only_enrich_usage { - let cookie_header = (cookie_source == "manual") - .then(|| active_token_cookie.clone().or(stored_cookie)) - .flatten(); - (usage_source, cookie_header, false) - } else { - match cookie_source { - _ if manual_cookie_wins => (SourceMode::Web, stored_cookie.clone(), false), - _ if active_token_env.is_some() => (SourceMode::OAuth, None, false), - // Opt-in web providers keep their default credential lane - // unless the usage source is explicitly Web; a stored or - // browser cookie must not turn Auto into Web. - _ if provider.web_is_opt_in() && usage_source != SourceMode::Web => { - (usage_source, None, false) - } - // Charm Hyper: the cookie source only picks the session, and - // the usage source keeps routing. Off and an empty Manual - // source never import a browser session, while Auto keeps its - // API-key fallback. - "off" | "manual" if provider.cookie_source_scopes_session_only() => { - let cookie_header = if cookie_source == "manual" { - active_token_cookie.clone().or(stored_cookie) - } else { - None - }; - let source_mode = if provider.available_sources().contains(&usage_source) { - usage_source - } else { - SourceMode::Auto - }; - let cookie_missing = cookie_header.is_none(); - (source_mode, cookie_header, cookie_missing) - } - "off" if provider_uses_oauth_without_cookies(id, usage_source) => { - (SourceMode::OAuth, None, false) - } - "off" - if (has_kimi_code_api_key || has_opencodego_api_key) - && usage_source == SourceMode::Auto => - { - (SourceMode::Auto, None, false) - } - // Cookie-off must never scrape browser cookies (Droid/Factory). Map to - // Cli (API-only in the provider) so Auto does not fall through to web. - "off" => (SourceMode::Cli, None, false), - "manual" => { - let cookie_header = active_token_cookie.clone().or(stored_cookie); - let fails_closed_without_cookie = cookie_header - .as_deref() - .is_none_or(|header| header.trim().is_empty()) - && provider.manual_empty_cookie_policy() - == ManualEmptyCookiePolicy::FailClosedWeb; - let source_mode = if (has_kimi_code_api_key || has_opencodego_api_key) - && usage_source == SourceMode::Auto - { - SourceMode::Auto - } else if let Some(mode) = grok_source_mode_for_manual_cookie(id, usage_source) { - // Grok Switch writes ~/.grok/auth.json. Leftover grok.com - // cookies must not force Web, or Weekly/notifications keep - // showing the previous browser account. - mode - } else if cookie_header.is_some() { - SourceMode::Web - } else if fails_closed_without_cookie { - // The provider owns this policy; Web with no header means - // it fails closed instead of importing a browser account - // the user did not select. - SourceMode::Web - } else if provider_uses_oauth_without_cookies(id, usage_source) { - SourceMode::OAuth - } else { - SourceMode::Cli - }; - (source_mode, cookie_header, fails_closed_without_cookie) - } - // `browser` is accepted as a legacy alias from older settings. - "auto" | "browser" | "web" => { - // Claude resolves its cached cookie and browser fallback inside - // the provider; other providers retain the shell fallback. - let cookie_header = active_token_cookie.clone().or(stored_cookie).or_else(|| { - (!defer_provider_browser_cookie_lookup) - .then(|| browser_cookie_header(id, settings)) - .flatten() - }); - (usage_source, cookie_header, false) - } - _ => (usage_source, stored_cookie, false), - } - }; - - // Cookie-web providers (Cursor, OpenCode, …) reject SourceMode::Cli. The shell - // historically mapped "manual + no cookie" to Cli, which surfaces as - // "Source mode 'Cli' not supported". Remap to Web and try browser cookies - // unless the user explicitly disabled cookies ("off"). Providers whose - // cookie source only scopes the session (Charm Hyper) or only enriches an - // API result (Muse browser team quota) own this contract in the provider, - // so the shell must not remap their source mode. - if source_mode == SourceMode::Cli - && cookie_source != "off" - && !provider.supports_cli() - && !provider.cookie_source_scopes_session_only() - && !cookies_only_enrich_usage - { - if cookie_header - .as_deref() - .map(str::trim) - .is_none_or(|s| s.is_empty()) - { - cookie_header = browser_cookie_header(id, settings); - } - source_mode = SourceMode::Web; - } - - // The inverse case: some providers never fetch over the web and reject - // SourceMode::Web outright (Codex since the 0.54 port only does OAuth/PAT/CLI). - // The default cookie source is "manual", so a pasted chatgpt.com cookie flipped - // Codex into Web and every refresh failed with - // "Source mode 'Web' not supported for this provider". Fall back to the - // configured usage source (or Auto) instead of handing the provider a mode it - // advertises as unsupported. - let available_sources = provider.available_sources(); - if source_mode == SourceMode::Web && !available_sources.contains(&SourceMode::Web) { - source_mode = if available_sources.contains(&usage_source) { - usage_source - } else { - SourceMode::Auto - }; - } - - let workspace_id = settings.workspace_id(id).trim().to_string(); - let api_region = settings.api_region(id).trim().to_string(); - // Every gateway-style provider (Wayfinder, Bifrost, Aixy) stores its base - // URL here; providers without one report an empty string. - let gateway_url = Some(settings.gateway_url(id)) - .filter(|url| !url.is_empty()) - .map(str::to_owned); - // Local-first Auto providers (OpenCode Go) flip to web-first when a - // token account or manual cookie source scopes the session to web creds. - let auto_prefer_web = token_override.is_some() || cookie_source == "manual"; - - // These upstream account types are explicit identity selections. Keep the - // provider's saved region/source settings intact, but project the selected - // credential into the route required by that account. - let (cookie_header, api_key) = match (id, token_account_kind, usage_source) { - (ProviderId::Kimi, Some(_), _) => (active_token_cookie.clone(), None), - (ProviderId::Doubao, Some(_), _) => (None, active_token_api_key.clone()), - ( - ProviderId::OpenCodeGo, - Some(codexbar::core::TokenAccountKind::ApiKey), - SourceMode::Auto, - ) => (None, active_token_api_key.clone()), - (ProviderId::OpenCodeGo, Some(codexbar::core::TokenAccountKind::ApiKey), _) => { - (cookie_header, api_key) - } - ( - ProviderId::OpenCodeGo, - Some(codexbar::core::TokenAccountKind::Cookie), - SourceMode::Auto, - ) => (active_token_cookie.clone(), api_key), - _ => (cookie_header, api_key), - }; - let source_mode = token_override - .as_ref() - .and_then(|account| account.effective_source_mode(usage_source)) - .unwrap_or(source_mode); - let token_account_isolated = token_override.is_some() - && matches!( - id, - ProviderId::Kimi | ProviderId::Doubao | ProviderId::OpenCodeGo - ); - - FetchContext { - source_mode, - manual_cookie_header: cookie_header, - manual_cookie_missing: fails_closed_without_cookie, - api_key, - token_account_kind, - token_account_isolated, - workspace_id: (!workspace_id.is_empty()).then_some(workspace_id), - seat_credit_entitlement: settings.seat_credit_entitlement(id), - api_region: (!api_region.is_empty()).then_some(api_region), - gateway_url, - auto_prefer_web: auto_prefer_web - && !(id == ProviderId::OpenCodeGo - && token_account_kind == Some(codexbar::core::TokenAccountKind::ApiKey)), - browser_cookie_import, - optional_details_enabled: settings.optional_details_enabled(id), - ..FetchContext::default() - } -} - -fn browser_cookie_header(id: ProviderId, settings: &Settings) -> Option { - provider_cookie_domain(id, settings).and_then(|domain| { - codexbar::browser::cookies::get_cookie_header(domain) - .ok() - .filter(|h| !h.is_empty()) - }) -} - -fn provider_uses_oauth_without_cookies(id: ProviderId, usage_source: SourceMode) -> bool { - match id { - ProviderId::Claude => usage_source != SourceMode::Cli, - ProviderId::Grok => matches!(usage_source, SourceMode::Auto | SourceMode::OAuth), - _ => false, - } -} - -/// Keep Grok Auto/OAuth/Cli on the switched login instead of rewriting a -/// leftover manual cookie as Web. Auto remains Auto even when its manual -/// cookie is empty; the provider uses that context to skip browser refresh. -fn grok_source_mode_for_manual_cookie( - id: ProviderId, - usage_source: SourceMode, -) -> Option { - if id != ProviderId::Grok { - return None; - } - match usage_source { - SourceMode::Cli => Some(SourceMode::Cli), - SourceMode::Auto => Some(SourceMode::Auto), - SourceMode::OAuth => Some(SourceMode::OAuth), - _ => None, - } -} - -pub(crate) fn provider_cookie_domain(id: ProviderId, settings: &Settings) -> Option<&'static str> { - if id == ProviderId::MiniMax { - return Some( - codexbar::providers::MiniMaxProvider::cookie_domain_for_region(Some( - settings.api_region(id), - )), - ); - } - if id == ProviderId::Alibaba { - return Some( - codexbar::providers::AlibabaProvider::cookie_domain_for_region(Some( - settings.api_region(id), - )), - ); - } - id.cookie_domain() -} - -const DEFAULT_PROVIDER_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(35); -const SLOW_PROVIDER_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(75); -const OPTIONAL_LITELLM_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(40); -const MAX_CONTEXT_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(65); - -pub(crate) fn provider_fetch_timeout(id: ProviderId, ctx: &FetchContext) -> std::time::Duration { - let provider_timeout = match id { - ProviderId::Claude | ProviderId::Codex | ProviderId::Copilot => SLOW_PROVIDER_FETCH_TIMEOUT, - ProviderId::LiteLLM if ctx.optional_details_enabled => OPTIONAL_LITELLM_FETCH_TIMEOUT, - _ => DEFAULT_PROVIDER_FETCH_TIMEOUT, - }; - let context_timeout = std::time::Duration::from_secs(ctx.web_timeout.saturating_add(5)); - provider_timeout.max(context_timeout.min(MAX_CONTEXT_FETCH_TIMEOUT)) -} - pub(crate) fn upsert_provider_cache( cache: &mut Vec, snapshot: ProviderUsageSnapshot, @@ -990,248 +659,6 @@ fn record_provider_fetch_duration( } } -fn update_tray_and_notifications( - app: &tauri::AppHandle, - state: &tauri::State<'_, Mutex>, - settings: &Settings, - token_accounts: &HashMap, -) -> Result<(), String> { - let cached = { - let guard = state.lock().map_err(|e| e.to_string())?; - guard.provider_cache.clone() - }; - crate::tray_bridge::update_tray_status_items(app, &cached); - crate::tray_bridge::update_tray_icon_and_tooltip(app, &cached); - notify_usage_thresholds(state, settings, token_accounts, &cached); - Ok(()) -} - -fn notify_usage_thresholds( - state: &tauri::State<'_, Mutex>, - settings: &Settings, - token_accounts: &HashMap, - cached: &[ProviderUsageSnapshot], -) { - let cli_map = codexbar::core::cli_name_map(); - if let Ok(mut guard) = state.lock() { - for snapshot in cached { - if snapshot.error.is_none() - && let Some(&provider) = cli_map.get(snapshot.provider_id.as_str()) - { - let token_account_id = token_accounts - .get(&provider) - .and_then(ProviderAccountData::active_account) - .map(|account| account.id); - let warning_identity = WarningIdentity::new( - provider, - &snapshot.source_label, - snapshot.account_email.as_deref(), - snapshot.account_organization.as_deref(), - token_account_id, - ); - // Hooks keep their own per-source baselines (edge-triggered, first sample - // never fires), so they stay on the source key; only toast dedupe below - // bridges account-identity gaps. - let account = warning_identity.threshold_key(); - let scope = warning_identity.gap_scope(); - // Skip all session consumers for synthetic/no-session - // placeholders (e.g. Claude OAuth five_hour: null). - let session_account = resolve_toast_account( - &mut guard.notification_manager, - provider, - &scope, - "session", - &snapshot.primary, - settings, - ); - if guard.notification_manager.check_session_lane( - provider, - &session_account, - snapshot.primary.used_percent, - snapshot.primary.is_informational, - settings, - ) { - dispatch_quota_hooks( - settings, - provider, - &account, - "session", - snapshot.primary.used_percent, - ); - } - if let Some(weekly) = &snapshot.secondary - && !weekly.is_informational - { - let weekly_account = resolve_toast_account( - &mut guard.notification_manager, - provider, - &scope, - "weekly", - weekly, - settings, - ); - guard.notification_manager.check_and_notify( - provider, - &weekly_account, - "weekly", - weekly.used_percent, - settings, - ); - dispatch_quota_hooks( - settings, - provider, - &account, - "weekly", - weekly.used_percent, - ); - } - notify_predictive_pace( - &mut guard.notification_manager, - provider, - snapshot, - token_accounts, - settings, - ); - } - } - } -} - -/// Account key a toast lane is deduped under (see `NotificationManager::resolve_warning_account`). -/// Informational placeholders are not observed, matching `check_session_lane`. -fn resolve_toast_account( - manager: &mut codexbar::notifications::NotificationManager, - provider: ProviderId, - scope: &WarningScope, - window: &str, - lane: &RateWindowSnapshot, - settings: &Settings, -) -> String { - if lane.is_informational { - return scope.key().to_string(); - } - let resets_at = lane.resets_at_utc(); - manager.resolve_warning_account( - provider, - scope, - window, - lane.used_percent, - resets_at, - settings, - ) -} - -fn dispatch_quota_hooks( - settings: &Settings, - provider: ProviderId, - account: &str, - window: &str, - used_percent: f64, -) { - if !settings.hooks_enabled { - return; - } - let thresholds = settings.usage_thresholds(provider, window); - let account = if settings.hide_personal_info || account.is_empty() { - None - } else { - Some(account) - }; - codexbar::core::emit_quota_threshold_hooks( - true, - provider.cli_name(), - window, - used_percent, - thresholds.high, - thresholds.critical, - account, - ); -} - -/// Stable account discriminator for threshold/session toast dedupe. -/// Prefer token-account id, then email, org, plan; empty for single-account lanes. -pub(super) fn quota_notification_account_identity( - snapshot: &ProviderUsageSnapshot, - token_account_id: Option, -) -> String { - ProviderId::from_cli_name(&snapshot.provider_id) - .map(|provider| { - WarningIdentity::new( - provider, - &snapshot.source_label, - snapshot.account_email.as_deref(), - snapshot.account_organization.as_deref(), - token_account_id, - ) - .threshold_key() - }) - .unwrap_or_default() -} - -fn notify_predictive_pace( - manager: &mut codexbar::notifications::NotificationManager, - provider: ProviderId, - snapshot: &ProviderUsageSnapshot, - token_accounts: &HashMap, - settings: &Settings, -) { - let enabled = settings.show_notifications && settings.predictive_pace_warning_enabled; - manager.set_predictive_warnings_enabled(provider, enabled); - if !enabled || !matches!(provider, ProviderId::Claude | ProviderId::Codex) { - return; - } - - let token_account_id = token_accounts - .get(&provider) - .and_then(ProviderAccountData::active_account) - .map(|account| account.id); - let warning_identity = WarningIdentity::new( - provider, - &snapshot.source_label, - snapshot.account_email.as_deref(), - None, - token_account_id, - ); - let Some(identity) = warning_identity.predictive_key() else { - return; - }; - let observed_at = parse_utc(&snapshot.updated_at); - - for (warning_window, window, default_window_minutes) in [ - ( - codexbar::notifications::PredictiveWarningWindow::Session, - Some(&snapshot.primary), - 300, - ), - ( - codexbar::notifications::PredictiveWarningWindow::Weekly, - snapshot.secondary.as_ref(), - 10080, - ), - ] { - let Some(window) = window else { - continue; - }; - if window.is_informational { - continue; - } - let rate_window = window.to_rate_window(); - let Some(pace) = - codexbar::core::UsagePace::weekly(&rate_window, observed_at, default_window_minutes) - else { - continue; - }; - manager.check_predictive_pace( - provider, - &identity, - warning_window, - &rate_window, - &pace, - settings, - ); - } -} - #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct DeepSeekPricingStatus { diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers/fetch_context.rs b/apps/desktop-tauri/src-tauri/src/commands/providers/fetch_context.rs new file mode 100644 index 00000000000..a83e842141d --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/providers/fetch_context.rs @@ -0,0 +1,338 @@ +use super::*; + +/// Build a `FetchContext` for a provider using persisted cookies/keys. +pub(crate) fn build_fetch_context( + id: ProviderId, + settings: &Settings, + cookies: &ManualCookies, + api_keys: &ApiKeys, + token_accounts: &HashMap, +) -> FetchContext { + let provider = instantiate_provider(id); + let cookie_source = settings.cookie_source(id); + let stored_cookie = cookies.get(id.cli_name()).map(|s| s.to_string()); + let stored_api_key = api_keys.get(id.cli_name()).map(|s| s.to_string()); + let token_override = token_accounts + .get(&id) + .and_then(|data| data.active_account()) + .cloned() + .map(|account| TokenAccountOverride::from_account(id, account)); + let active_token_cookie = token_override + .as_ref() + .and_then(|override_data| override_data.cookie_header.clone()); + let defer_provider_browser_cookie_lookup = provider.owns_browser_cookie_resolution() + && active_token_cookie.is_none() + && stored_cookie.is_none(); + let active_token_env = token_override + .as_ref() + .and_then(|override_data| override_data.env_override.as_ref()); + let active_token_api_key = active_token_env.and_then(|env| env.values().next().cloned()); + let usage_source = SourceMode::parse(settings.usage_source(id)).unwrap_or_default(); + let token_account_kind = token_override.as_ref().map(|account| account.kind); + // Selected token-account key overrides a stored provider apiKey (upstream #2271 / #1183). + let api_key = active_token_api_key.clone().or(stored_api_key); + let has_kimi_code_api_key = + id == ProviderId::Kimi && api_key.as_deref().is_some_and(|key| !key.trim().is_empty()); + let has_opencodego_api_key = id == ProviderId::OpenCodeGo + && api_key.as_deref().is_some_and(|key| !key.trim().is_empty()); + + // Providers whose cookies only enrich an API result keep the configured + // usage source. Off and the default manual-without-cookie state never read + // a browser; only Automatic imports one (`browser_cookie_import`). + let cookies_only_enrich_usage = provider.cookies_only_enrich_usage(); + let browser_cookie_import = + cookies_only_enrich_usage && matches!(cookie_source, "auto" | "browser" | "web"); + // #433: an explicitly selected, non-empty Claude manual cookie is + // authoritative. Do not let an active OAuth token account silently + // replace it; this keeps tray refresh behavior aligned with diagnose, + // whose Claude Auto path tries the supplied Web cookie before OAuth. + let manual_cookie_wins = cookie_source == "manual" + && provider.manual_cookie_precedes_token_account() + && stored_cookie + .as_deref() + .is_some_and(|cookie| !cookie.trim().is_empty()); + let (mut source_mode, mut cookie_header, fails_closed_without_cookie) = if id + .cookie_domain() + .is_none() + { + // The #725 wallet rule (#725 keeps Auto for wallet providers) folds into + // the account-source decision; other no-cookie providers still honor + // cookie_source below (the account-source pack's fix for the #619 merge). + // A wallet provider keeps its saved usage source even with a selected + // token account: rewriting it to OAuth would skip the wallet read + // (#725 / upstream base-source resolver). + let keeps_auto = + usage_source == SourceMode::Auto && provider.token_account_preserves_auto_source(); + if active_token_env.is_some() && keeps_auto { + (usage_source, None, false) + } else if active_token_env.is_some() + && provider.web_is_opt_in() + && usage_source != SourceMode::Web + { + // Opt-in web providers keep their default credential lane + // unless the usage source is explicitly Web. + (usage_source, None, false) + } else if manual_cookie_wins { + (SourceMode::Web, stored_cookie.clone(), false) + } else if active_token_env.is_some() { + (SourceMode::OAuth, None, false) + } else { + (usage_source, None, false) + } + } else if cookies_only_enrich_usage { + let cookie_header = (cookie_source == "manual") + .then(|| active_token_cookie.clone().or(stored_cookie)) + .flatten(); + (usage_source, cookie_header, false) + } else { + match cookie_source { + _ if manual_cookie_wins => (SourceMode::Web, stored_cookie.clone(), false), + _ if active_token_env.is_some() => (SourceMode::OAuth, None, false), + // Opt-in web providers keep their default credential lane + // unless the usage source is explicitly Web; a stored or + // browser cookie must not turn Auto into Web. + _ if provider.web_is_opt_in() && usage_source != SourceMode::Web => { + (usage_source, None, false) + } + // Charm Hyper: the cookie source only picks the session, and + // the usage source keeps routing. Off and an empty Manual + // source never import a browser session, while Auto keeps its + // API-key fallback. + "off" | "manual" if provider.cookie_source_scopes_session_only() => { + let cookie_header = if cookie_source == "manual" { + active_token_cookie.clone().or(stored_cookie) + } else { + None + }; + let source_mode = if provider.available_sources().contains(&usage_source) { + usage_source + } else { + SourceMode::Auto + }; + let cookie_missing = cookie_header.is_none(); + (source_mode, cookie_header, cookie_missing) + } + "off" if provider_uses_oauth_without_cookies(id, usage_source) => { + (SourceMode::OAuth, None, false) + } + "off" + if (has_kimi_code_api_key || has_opencodego_api_key) + && usage_source == SourceMode::Auto => + { + (SourceMode::Auto, None, false) + } + // Cookie-off must never scrape browser cookies (Droid/Factory). Map to + // Cli (API-only in the provider) so Auto does not fall through to web. + "off" => (SourceMode::Cli, None, false), + "manual" => { + let cookie_header = active_token_cookie.clone().or(stored_cookie); + let fails_closed_without_cookie = cookie_header + .as_deref() + .is_none_or(|header| header.trim().is_empty()) + && provider.manual_empty_cookie_policy() + == ManualEmptyCookiePolicy::FailClosedWeb; + let source_mode = if (has_kimi_code_api_key || has_opencodego_api_key) + && usage_source == SourceMode::Auto + { + SourceMode::Auto + } else if let Some(mode) = grok_source_mode_for_manual_cookie(id, usage_source) { + // Grok Switch writes ~/.grok/auth.json. Leftover grok.com + // cookies must not force Web, or Weekly/notifications keep + // showing the previous browser account. + mode + } else if cookie_header.is_some() { + SourceMode::Web + } else if fails_closed_without_cookie { + // The provider owns this policy; Web with no header means + // it fails closed instead of importing a browser account + // the user did not select. + SourceMode::Web + } else if provider_uses_oauth_without_cookies(id, usage_source) { + SourceMode::OAuth + } else { + SourceMode::Cli + }; + (source_mode, cookie_header, fails_closed_without_cookie) + } + // `browser` is accepted as a legacy alias from older settings. + "auto" | "browser" | "web" => { + // Claude resolves its cached cookie and browser fallback inside + // the provider; other providers retain the shell fallback. + let cookie_header = active_token_cookie.clone().or(stored_cookie).or_else(|| { + (!defer_provider_browser_cookie_lookup) + .then(|| browser_cookie_header(id, settings)) + .flatten() + }); + (usage_source, cookie_header, false) + } + _ => (usage_source, stored_cookie, false), + } + }; + + // Cookie-web providers (Cursor, OpenCode, …) reject SourceMode::Cli. The shell + // historically mapped "manual + no cookie" to Cli, which surfaces as + // "Source mode 'Cli' not supported". Remap to Web and try browser cookies + // unless the user explicitly disabled cookies ("off"). Providers whose + // cookie source only scopes the session (Charm Hyper) or only enriches an + // API result (Muse browser team quota) own this contract in the provider, + // so the shell must not remap their source mode. + if source_mode == SourceMode::Cli + && cookie_source != "off" + && !provider.supports_cli() + && !provider.cookie_source_scopes_session_only() + && !cookies_only_enrich_usage + { + if cookie_header + .as_deref() + .map(str::trim) + .is_none_or(|s| s.is_empty()) + { + cookie_header = browser_cookie_header(id, settings); + } + source_mode = SourceMode::Web; + } + + // The inverse case: some providers never fetch over the web and reject + // SourceMode::Web outright (Codex since the 0.54 port only does OAuth/PAT/CLI). + // The default cookie source is "manual", so a pasted chatgpt.com cookie flipped + // Codex into Web and every refresh failed with + // "Source mode 'Web' not supported for this provider". Fall back to the + // configured usage source (or Auto) instead of handing the provider a mode it + // advertises as unsupported. + let available_sources = provider.available_sources(); + if source_mode == SourceMode::Web && !available_sources.contains(&SourceMode::Web) { + source_mode = if available_sources.contains(&usage_source) { + usage_source + } else { + SourceMode::Auto + }; + } + + let workspace_id = settings.workspace_id(id).trim().to_string(); + let api_region = settings.api_region(id).trim().to_string(); + // Every gateway-style provider (Wayfinder, Bifrost, Aixy) stores its base + // URL here; providers without one report an empty string. + let gateway_url = Some(settings.gateway_url(id)) + .filter(|url| !url.is_empty()) + .map(str::to_owned); + // Local-first Auto providers (OpenCode Go) flip to web-first when a + // token account or manual cookie source scopes the session to web creds. + let auto_prefer_web = token_override.is_some() || cookie_source == "manual"; + + // These upstream account types are explicit identity selections. Keep the + // provider's saved region/source settings intact, but project the selected + // credential into the route required by that account. + let (cookie_header, api_key) = match (id, token_account_kind, usage_source) { + (ProviderId::Kimi, Some(_), _) => (active_token_cookie.clone(), None), + (ProviderId::Doubao, Some(_), _) => (None, active_token_api_key.clone()), + ( + ProviderId::OpenCodeGo, + Some(codexbar::core::TokenAccountKind::ApiKey), + SourceMode::Auto, + ) => (None, active_token_api_key.clone()), + (ProviderId::OpenCodeGo, Some(codexbar::core::TokenAccountKind::ApiKey), _) => { + (cookie_header, api_key) + } + ( + ProviderId::OpenCodeGo, + Some(codexbar::core::TokenAccountKind::Cookie), + SourceMode::Auto, + ) => (active_token_cookie.clone(), api_key), + _ => (cookie_header, api_key), + }; + let source_mode = token_override + .as_ref() + .and_then(|account| account.effective_source_mode(usage_source)) + .unwrap_or(source_mode); + let token_account_isolated = token_override.is_some() + && matches!( + id, + ProviderId::Kimi | ProviderId::Doubao | ProviderId::OpenCodeGo + ); + + FetchContext { + source_mode, + manual_cookie_header: cookie_header, + manual_cookie_missing: fails_closed_without_cookie, + api_key, + token_account_kind, + token_account_isolated, + workspace_id: (!workspace_id.is_empty()).then_some(workspace_id), + seat_credit_entitlement: settings.seat_credit_entitlement(id), + api_region: (!api_region.is_empty()).then_some(api_region), + gateway_url, + auto_prefer_web: auto_prefer_web + && !(id == ProviderId::OpenCodeGo + && token_account_kind == Some(codexbar::core::TokenAccountKind::ApiKey)), + browser_cookie_import, + optional_details_enabled: settings.optional_details_enabled(id), + ..FetchContext::default() + } +} + +fn browser_cookie_header(id: ProviderId, settings: &Settings) -> Option { + provider_cookie_domain(id, settings).and_then(|domain| { + codexbar::browser::cookies::get_cookie_header(domain) + .ok() + .filter(|h| !h.is_empty()) + }) +} + +fn provider_uses_oauth_without_cookies(id: ProviderId, usage_source: SourceMode) -> bool { + match id { + ProviderId::Claude => usage_source != SourceMode::Cli, + ProviderId::Grok => matches!(usage_source, SourceMode::Auto | SourceMode::OAuth), + _ => false, + } +} + +/// Keep Grok Auto/OAuth/Cli on the switched login instead of rewriting a +/// leftover manual cookie as Web. Auto remains Auto even when its manual +/// cookie is empty; the provider uses that context to skip browser refresh. +fn grok_source_mode_for_manual_cookie( + id: ProviderId, + usage_source: SourceMode, +) -> Option { + if id != ProviderId::Grok { + return None; + } + match usage_source { + SourceMode::Cli => Some(SourceMode::Cli), + SourceMode::Auto => Some(SourceMode::Auto), + SourceMode::OAuth => Some(SourceMode::OAuth), + _ => None, + } +} + +pub(crate) fn provider_cookie_domain(id: ProviderId, settings: &Settings) -> Option<&'static str> { + if id == ProviderId::MiniMax { + return Some( + codexbar::providers::MiniMaxProvider::cookie_domain_for_region(Some( + settings.api_region(id), + )), + ); + } + if id == ProviderId::Alibaba { + return Some( + codexbar::providers::AlibabaProvider::cookie_domain_for_region(Some( + settings.api_region(id), + )), + ); + } + id.cookie_domain() +} + +const DEFAULT_PROVIDER_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(35); +const SLOW_PROVIDER_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(75); +const OPTIONAL_LITELLM_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(40); +const MAX_CONTEXT_FETCH_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(65); + +pub(crate) fn provider_fetch_timeout(id: ProviderId, ctx: &FetchContext) -> std::time::Duration { + let provider_timeout = match id { + ProviderId::Claude | ProviderId::Codex | ProviderId::Copilot => SLOW_PROVIDER_FETCH_TIMEOUT, + ProviderId::LiteLLM if ctx.optional_details_enabled => OPTIONAL_LITELLM_FETCH_TIMEOUT, + _ => DEFAULT_PROVIDER_FETCH_TIMEOUT, + }; + let context_timeout = std::time::Duration::from_secs(ctx.web_timeout.saturating_add(5)); + provider_timeout.max(context_timeout.min(MAX_CONTEXT_FETCH_TIMEOUT)) +} diff --git a/apps/desktop-tauri/src-tauri/src/commands/providers/notify.rs b/apps/desktop-tauri/src-tauri/src/commands/providers/notify.rs new file mode 100644 index 00000000000..39516d4e77d --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/providers/notify.rs @@ -0,0 +1,243 @@ +use super::*; + +pub(super) fn update_tray_and_notifications( + app: &tauri::AppHandle, + state: &tauri::State<'_, Mutex>, + settings: &Settings, + token_accounts: &HashMap, +) -> Result<(), String> { + let cached = { + let guard = state.lock().map_err(|e| e.to_string())?; + guard.provider_cache.clone() + }; + crate::tray_bridge::update_tray_status_items(app, &cached); + crate::tray_bridge::update_tray_icon_and_tooltip(app, &cached); + notify_usage_thresholds(state, settings, token_accounts, &cached); + Ok(()) +} + +fn notify_usage_thresholds( + state: &tauri::State<'_, Mutex>, + settings: &Settings, + token_accounts: &HashMap, + cached: &[ProviderUsageSnapshot], +) { + let cli_map = codexbar::core::cli_name_map(); + if let Ok(mut guard) = state.lock() { + for snapshot in cached { + if snapshot.error.is_none() + && let Some(&provider) = cli_map.get(snapshot.provider_id.as_str()) + { + let token_account_id = token_accounts + .get(&provider) + .and_then(ProviderAccountData::active_account) + .map(|account| account.id); + let warning_identity = WarningIdentity::new( + provider, + &snapshot.source_label, + snapshot.account_email.as_deref(), + snapshot.account_organization.as_deref(), + token_account_id, + ); + // Hooks keep their own per-source baselines (edge-triggered, first sample + // never fires), so they stay on the source key; only toast dedupe below + // bridges account-identity gaps. + let account = warning_identity.threshold_key(); + let scope = warning_identity.gap_scope(); + // Skip all session consumers for synthetic/no-session + // placeholders (e.g. Claude OAuth five_hour: null). + let session_account = resolve_toast_account( + &mut guard.notification_manager, + provider, + &scope, + "session", + &snapshot.primary, + settings, + ); + if guard.notification_manager.check_session_lane( + provider, + &session_account, + snapshot.primary.used_percent, + snapshot.primary.is_informational, + settings, + ) { + dispatch_quota_hooks( + settings, + provider, + &account, + "session", + snapshot.primary.used_percent, + ); + } + if let Some(weekly) = &snapshot.secondary + && !weekly.is_informational + { + let weekly_account = resolve_toast_account( + &mut guard.notification_manager, + provider, + &scope, + "weekly", + weekly, + settings, + ); + guard.notification_manager.check_and_notify( + provider, + &weekly_account, + "weekly", + weekly.used_percent, + settings, + ); + dispatch_quota_hooks( + settings, + provider, + &account, + "weekly", + weekly.used_percent, + ); + } + notify_predictive_pace( + &mut guard.notification_manager, + provider, + snapshot, + token_accounts, + settings, + ); + } + } + } +} + +/// Account key a toast lane is deduped under (see `NotificationManager::resolve_warning_account`). +/// Informational placeholders are not observed, matching `check_session_lane`. +fn resolve_toast_account( + manager: &mut codexbar::notifications::NotificationManager, + provider: ProviderId, + scope: &WarningScope, + window: &str, + lane: &RateWindowSnapshot, + settings: &Settings, +) -> String { + if lane.is_informational { + return scope.key().to_string(); + } + let resets_at = lane.resets_at_utc(); + manager.resolve_warning_account( + provider, + scope, + window, + lane.used_percent, + resets_at, + settings, + ) +} + +fn dispatch_quota_hooks( + settings: &Settings, + provider: ProviderId, + account: &str, + window: &str, + used_percent: f64, +) { + if !settings.hooks_enabled { + return; + } + let thresholds = settings.usage_thresholds(provider, window); + let account = if settings.hide_personal_info || account.is_empty() { + None + } else { + Some(account) + }; + codexbar::core::emit_quota_threshold_hooks( + true, + provider.cli_name(), + window, + used_percent, + thresholds.high, + thresholds.critical, + account, + ); +} + +/// Stable account discriminator for threshold/session toast dedupe. +/// Prefer token-account id, then email, org, plan; empty for single-account lanes. +pub(in crate::commands) fn quota_notification_account_identity( + snapshot: &ProviderUsageSnapshot, + token_account_id: Option, +) -> String { + ProviderId::from_cli_name(&snapshot.provider_id) + .map(|provider| { + WarningIdentity::new( + provider, + &snapshot.source_label, + snapshot.account_email.as_deref(), + snapshot.account_organization.as_deref(), + token_account_id, + ) + .threshold_key() + }) + .unwrap_or_default() +} + +fn notify_predictive_pace( + manager: &mut codexbar::notifications::NotificationManager, + provider: ProviderId, + snapshot: &ProviderUsageSnapshot, + token_accounts: &HashMap, + settings: &Settings, +) { + let enabled = settings.show_notifications && settings.predictive_pace_warning_enabled; + manager.set_predictive_warnings_enabled(provider, enabled); + if !enabled || !matches!(provider, ProviderId::Claude | ProviderId::Codex) { + return; + } + + let token_account_id = token_accounts + .get(&provider) + .and_then(ProviderAccountData::active_account) + .map(|account| account.id); + let warning_identity = WarningIdentity::new( + provider, + &snapshot.source_label, + snapshot.account_email.as_deref(), + None, + token_account_id, + ); + let Some(identity) = warning_identity.predictive_key() else { + return; + }; + let observed_at = parse_utc(&snapshot.updated_at); + + for (warning_window, window, default_window_minutes) in [ + ( + codexbar::notifications::PredictiveWarningWindow::Session, + Some(&snapshot.primary), + 300, + ), + ( + codexbar::notifications::PredictiveWarningWindow::Weekly, + snapshot.secondary.as_ref(), + 10080, + ), + ] { + let Some(window) = window else { + continue; + }; + if window.is_informational { + continue; + } + let rate_window = window.to_rate_window(); + let Some(pace) = + codexbar::core::UsagePace::weekly(&rate_window, observed_at, default_window_minutes) + else { + continue; + }; + manager.check_predictive_pace( + provider, + &identity, + warning_window, + &rate_window, + &pace, + settings, + ); + } +} From ab9c5be916f963d02dda5be71aa8bc425a76958a Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:28:43 +0700 Subject: [PATCH 16/22] Move the settings and bootstrap snapshots out of commands/bridge.rs --- .../src-tauri/src/commands/bridge.rs | 368 +----------------- .../src/commands/bridge/settings_snapshot.rs | 367 +++++++++++++++++ 2 files changed, 369 insertions(+), 366 deletions(-) create mode 100644 apps/desktop-tauri/src-tauri/src/commands/bridge/settings_snapshot.rs diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 005c742befd..72e64e6b1f6 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -3,9 +3,11 @@ mod openai_usage; mod openai_usage_tests; pub(crate) mod pace; mod quota_block; +mod settings_snapshot; mod status; pub(crate) use openai_usage::OpenAiApiUsageSnapshot; pub use quota_block::MonthlyLimitBlockSnapshot; +pub use settings_snapshot::*; pub(crate) use status::{compact_tray_status_label, friendly_provider_error}; use super::*; @@ -759,371 +761,5 @@ fn series_entries( .unwrap_or_default() } -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct BootstrapState { - pub(crate) contract_version: &'static str, - pub(crate) providers: Vec, - pub(crate) settings: SettingsSnapshot, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CurrentSurfaceState { - pub mode: String, - pub target: SurfaceTarget, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ProviderCatalogEntry { - pub(crate) id: String, - pub(crate) display_name: String, - pub(crate) cookie_domain: Option, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SettingsSnapshot { - preferred_currency_code: String, - enabled_providers: Vec, - provider_order: Vec, - refresh_interval_secs: u64, - adaptive_refresh: bool, - refresh_all_providers_on_menu_open: bool, - low_power_mode: bool, - low_power_mode_preference: &'static str, - start_at_login: bool, - start_minimized: bool, - show_notifications: bool, - sound_enabled: bool, - notification_sound_theme: codexbar::settings::NotificationSoundTheme, - notification_sound_paths: codexbar::settings::NotificationSoundPaths, - high_usage_threshold: f64, - critical_usage_threshold: f64, - provider_usage_thresholds: - std::collections::HashMap, - predictive_pace_warning_enabled: bool, - credential_expiry_notifications_enabled: bool, - show_pace: bool, - tray_icon_mode: &'static str, - stacked_tray_top_provider: Option, - stacked_tray_bottom_provider: Option, - switcher_shows_icons: bool, - menu_bar_shows_highest_usage: bool, - menu_bar_shows_percent: bool, - menu_bar_color_pace: bool, - show_as_used: bool, - show_all_token_accounts_in_menu: bool, - enable_animations: bool, - reset_time_relative: bool, - show_reset_when_exhausted: bool, - menu_bar_display_mode: String, - overview_layout: String, - hide_personal_info: bool, - update_channel: &'static str, - auto_download_updates: bool, - install_updates_on_quit: bool, - global_shortcut: String, - switcher_shortcuts: std::collections::BTreeMap, - codex_custom_sessions_dirs: Vec, - agent_sessions_enabled: bool, - stay_awake_enabled: bool, - agent_session_ssh_hosts: Vec, - hooks_enabled: bool, - http_proxy_enabled: bool, - http_proxy_url: String, - http_proxy_username: String, - http_proxy_password: String, - ui_language: &'static str, - theme: &'static str, - window_scale_percent: u16, - tray_scale_percent: u16, - tray_panel_always_on_top: bool, - powertoys_status_pipe_enabled: bool, - claude_avoid_keychain_prompts: bool, - claude_swap_enabled: bool, - claude_swap_executable_path: String, - codex_spark_usage_visible: bool, - disable_keychain_access: bool, - wayfinder_gateway_url: String, - provider_metrics: std::collections::HashMap, - float_bar_enabled: bool, - float_bar_opacity: u8, - float_bar_scale: u8, - float_bar_orientation: String, - float_bar_style: String, - float_bar_click_through: bool, - float_bar_provider_ids: Vec, - float_bar_dark_text: bool, - float_bar_show_reset_inline: bool, - float_bar_show_cost: bool, - promote_tray_icon: bool, - claude_daily_routines_usage_visible: bool, - claude_allow_reading_claude_code_credentials: bool, - alibaba_token_plan_region: String, - copilot_seat_credit_entitlement: Option, - weekly_progress_work_days: Option, - cost_summary_display_style: &'static str, - open_codex_usage_logs_enabled: bool, - hide_native_codex_cost_when_open_codex_present: bool, - /// History window as its persisted raw form (`rolling:N`, - /// `month-to-date`, `all`). - cost_reporting_period: String, - provider_accent_colors: std::collections::HashMap, -} - -#[tauri::command] -pub fn get_bootstrap_state() -> BootstrapState { - bootstrap_state_for(Settings::load()) -} - -pub(crate) fn bootstrap_state_for(settings: Settings) -> BootstrapState { - BootstrapState { - contract_version: "v1", - providers: provider_catalog_for(&settings), - settings: SettingsSnapshot::from(settings), - } -} - -#[tauri::command] -pub fn get_provider_catalog() -> Vec { - provider_catalog_for(&Settings::load()) -} - -#[tauri::command] -pub fn get_settings_snapshot() -> SettingsSnapshot { - SettingsSnapshot::from(Settings::load()) -} - -impl From for SettingsSnapshot { - fn from(settings: Settings) -> Self { - let avoid_keychain_prompts = settings.claude_avoid_keychain_prompts(); - let claude_swap_enabled = settings.claude_swap_enabled(); - let claude_swap_executable_path = settings.claude_swap_executable_path().to_string(); - let codex_spark_usage_visible = settings.codex_spark_usage_visible(); - let wayfinder_gateway_url = settings.gateway_url(ProviderId::Wayfinder).to_string(); - - let provider_order = settings.provider_display_order_names(); - let enabled_providers = provider_order - .iter() - .filter(|provider_id| settings.enabled_providers.contains(*provider_id)) - .cloned() - .collect(); - - let copilot_seat_credit_entitlement = settings.seat_credit_entitlement(ProviderId::Copilot); - - let provider_metrics = settings - .provider_metrics - .into_iter() - .map(|(k, v)| (k, metric_preference_label(v))) - .collect(); - - Self { - preferred_currency_code: settings.preferred_currency_code, - enabled_providers, - provider_order, - refresh_interval_secs: settings.refresh_interval_secs, - adaptive_refresh: settings.adaptive_refresh, - refresh_all_providers_on_menu_open: settings.refresh_all_providers_on_menu_open, - low_power_mode: settings.low_power_mode_preference - == codexbar::settings::LowPowerModePreference::On, - low_power_mode_preference: settings.low_power_mode_preference.as_str(), - start_at_login: settings.start_at_login, - start_minimized: settings.start_minimized, - show_notifications: settings.show_notifications, - sound_enabled: settings.sound_enabled, - notification_sound_theme: settings.notification_sound_theme, - notification_sound_paths: settings.notification_sound_paths, - high_usage_threshold: settings.high_usage_threshold, - critical_usage_threshold: settings.critical_usage_threshold, - provider_usage_thresholds: settings.provider_usage_thresholds, - predictive_pace_warning_enabled: settings.predictive_pace_warning_enabled, - credential_expiry_notifications_enabled: settings - .credential_expiry_notifications_enabled, - show_pace: settings.show_pace, - tray_icon_mode: tray_icon_mode_label(settings.tray_icon_mode), - stacked_tray_top_provider: settings.stacked_tray_top_provider, - stacked_tray_bottom_provider: settings.stacked_tray_bottom_provider, - switcher_shows_icons: settings.switcher_shows_icons, - menu_bar_shows_highest_usage: settings.menu_bar_shows_highest_usage, - menu_bar_shows_percent: settings.menu_bar_shows_percent, - menu_bar_color_pace: settings.menu_bar_color_pace, - show_as_used: settings.show_as_used, - show_all_token_accounts_in_menu: settings.show_all_token_accounts_in_menu, - enable_animations: settings.enable_animations, - reset_time_relative: settings.reset_time_relative, - show_reset_when_exhausted: settings.show_reset_when_exhausted, - menu_bar_display_mode: settings.menu_bar_display_mode, - overview_layout: settings.overview_layout, - hide_personal_info: settings.hide_personal_info, - update_channel: update_channel_label(settings.update_channel), - auto_download_updates: settings.auto_download_updates, - install_updates_on_quit: settings.install_updates_on_quit, - switcher_shortcuts: codexbar::switcher_shortcuts::resolve_or_default( - &settings.switcher_shortcuts, - ), - global_shortcut: settings.global_shortcut, - codex_custom_sessions_dirs: settings.codex_custom_sessions_dirs, - agent_sessions_enabled: settings.agent_sessions_enabled, - stay_awake_enabled: settings.stay_awake_enabled, - agent_session_ssh_hosts: settings.agent_session_ssh_hosts, - hooks_enabled: settings.hooks_enabled, - http_proxy_enabled: settings.http_proxy_enabled, - http_proxy_url: settings.http_proxy_url, - http_proxy_username: settings.http_proxy_username, - http_proxy_password: settings.http_proxy_password, - ui_language: language_label(settings.ui_language), - theme: theme_label(settings.theme), - window_scale_percent: settings.window_scale_percent, - tray_scale_percent: settings.tray_scale_percent, - tray_panel_always_on_top: settings.tray_panel_always_on_top, - powertoys_status_pipe_enabled: settings.powertoys_status_pipe_enabled, - claude_avoid_keychain_prompts: avoid_keychain_prompts, - claude_swap_enabled, - claude_swap_executable_path, - codex_spark_usage_visible, - disable_keychain_access: settings.disable_keychain_access, - wayfinder_gateway_url, - provider_metrics, - float_bar_enabled: settings.float_bar_enabled, - float_bar_opacity: settings.float_bar_opacity, - float_bar_scale: settings.float_bar_scale, - float_bar_orientation: settings.float_bar_orientation, - float_bar_style: settings.float_bar_style, - float_bar_click_through: settings.float_bar_click_through, - float_bar_provider_ids: settings.float_bar_provider_ids, - float_bar_dark_text: settings.float_bar_dark_text, - float_bar_show_reset_inline: settings.float_bar_show_reset_inline, - float_bar_show_cost: settings.float_bar_show_cost, - promote_tray_icon: settings.promote_tray_icon, - claude_daily_routines_usage_visible: settings.claude_daily_routines_usage_visible, - claude_allow_reading_claude_code_credentials: settings - .claude_allow_reading_claude_code_credentials, - alibaba_token_plan_region: settings.alibaba_token_plan_region, - copilot_seat_credit_entitlement, - weekly_progress_work_days: settings.weekly_progress_work_days, - cost_summary_display_style: cost_summary_display_style_label( - settings.cost_summary_display_style, - ), - open_codex_usage_logs_enabled: settings.open_codex_usage_logs_enabled, - hide_native_codex_cost_when_open_codex_present: settings - .hide_native_codex_cost_when_open_codex_present, - cost_reporting_period: settings.cost_reporting_period.raw(), - provider_accent_colors: settings - .provider_configs - .iter() - .filter_map(|(id, config)| { - config - .accent_color - .as_ref() - .map(|color| (id.cli_name().to_string(), color.clone())) - }) - .collect(), - } - } -} - -pub(crate) fn provider_catalog_for(settings: &Settings) -> Vec { - // Soft-removed providers (upstream #2254) stay hidden in Settings unless already enabled. - settings - .provider_display_order() - .into_iter() - .filter(|provider| settings.is_provider_listed(*provider)) - .map(|provider| ProviderCatalogEntry { - id: provider.cli_name().to_string(), - display_name: provider.display_name().to_string(), - cookie_domain: provider.cookie_domain().map(ToString::to_string), - }) - .collect() -} - -fn tray_icon_mode_label(mode: TrayIconMode) -> &'static str { - match mode { - TrayIconMode::Single => "single", - TrayIconMode::PerProvider => "perProvider", - TrayIconMode::Stacked => "stacked", - } -} - -pub(super) fn update_channel_label(channel: UpdateChannel) -> &'static str { - match channel { - UpdateChannel::Stable => "stable", - UpdateChannel::Beta => "beta", - } -} - -pub(super) fn language_label(language: Language) -> &'static str { - language.label() -} - -fn theme_label(theme: ThemePreference) -> &'static str { - match theme { - ThemePreference::Auto => "auto", - ThemePreference::Light => "light", - ThemePreference::Dark => "dark", - } -} - -fn cost_summary_display_style_label( - style: codexbar::settings::CostSummaryDisplayStyle, -) -> &'static str { - match style { - codexbar::settings::CostSummaryDisplayStyle::Compact => "compact", - codexbar::settings::CostSummaryDisplayStyle::Detailed => "detailed", - codexbar::settings::CostSummaryDisplayStyle::Hidden => "hidden", - } -} - -pub(crate) fn parse_cost_summary_display_style( - s: &str, -) -> Option { - use codexbar::settings::CostSummaryDisplayStyle; - match s { - "compact" => Some(CostSummaryDisplayStyle::Compact), - "detailed" => Some(CostSummaryDisplayStyle::Detailed), - "hidden" => Some(CostSummaryDisplayStyle::Hidden), - _ => None, - } -} - -pub(super) fn parse_theme(s: &str) -> Option { - match s { - "auto" => Some(ThemePreference::Auto), - "light" => Some(ThemePreference::Light), - "dark" => Some(ThemePreference::Dark), - _ => None, - } -} - -fn metric_preference_label(pref: MetricPreference) -> &'static str { - match pref { - MetricPreference::Automatic => "automatic", - MetricPreference::Session => "session", - MetricPreference::Weekly => "weekly", - MetricPreference::Model => "model", - MetricPreference::Tertiary => "tertiary", - MetricPreference::Credits => "credits", - MetricPreference::ExtraUsage => "extraUsage", - MetricPreference::MonthlyPlan => "monthlyPlan", - MetricPreference::Average => "average", - } -} - -pub(super) fn parse_metric_preference(s: &str) -> Option { - match s { - "automatic" => Some(MetricPreference::Automatic), - "session" => Some(MetricPreference::Session), - "weekly" => Some(MetricPreference::Weekly), - "model" => Some(MetricPreference::Model), - "tertiary" => Some(MetricPreference::Tertiary), - "credits" => Some(MetricPreference::Credits), - "extraUsage" | "extrausage" => Some(MetricPreference::ExtraUsage), - "monthlyPlan" | "monthlyplan" => Some(MetricPreference::MonthlyPlan), - "average" => Some(MetricPreference::Average), - _ => None, - } -} - #[cfg(test)] mod tests; diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge/settings_snapshot.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge/settings_snapshot.rs new file mode 100644 index 00000000000..2852003baad --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge/settings_snapshot.rs @@ -0,0 +1,367 @@ +use super::*; + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BootstrapState { + pub(crate) contract_version: &'static str, + pub(crate) providers: Vec, + pub(crate) settings: SettingsSnapshot, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CurrentSurfaceState { + pub mode: String, + pub target: SurfaceTarget, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderCatalogEntry { + pub(crate) id: String, + pub(crate) display_name: String, + pub(crate) cookie_domain: Option, +} + +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SettingsSnapshot { + preferred_currency_code: String, + enabled_providers: Vec, + provider_order: Vec, + refresh_interval_secs: u64, + adaptive_refresh: bool, + refresh_all_providers_on_menu_open: bool, + low_power_mode: bool, + low_power_mode_preference: &'static str, + start_at_login: bool, + start_minimized: bool, + show_notifications: bool, + sound_enabled: bool, + notification_sound_theme: codexbar::settings::NotificationSoundTheme, + notification_sound_paths: codexbar::settings::NotificationSoundPaths, + high_usage_threshold: f64, + critical_usage_threshold: f64, + provider_usage_thresholds: + std::collections::HashMap, + predictive_pace_warning_enabled: bool, + credential_expiry_notifications_enabled: bool, + show_pace: bool, + tray_icon_mode: &'static str, + stacked_tray_top_provider: Option, + stacked_tray_bottom_provider: Option, + switcher_shows_icons: bool, + menu_bar_shows_highest_usage: bool, + menu_bar_shows_percent: bool, + menu_bar_color_pace: bool, + show_as_used: bool, + show_all_token_accounts_in_menu: bool, + enable_animations: bool, + reset_time_relative: bool, + show_reset_when_exhausted: bool, + menu_bar_display_mode: String, + overview_layout: String, + hide_personal_info: bool, + update_channel: &'static str, + auto_download_updates: bool, + install_updates_on_quit: bool, + global_shortcut: String, + switcher_shortcuts: std::collections::BTreeMap, + codex_custom_sessions_dirs: Vec, + agent_sessions_enabled: bool, + stay_awake_enabled: bool, + agent_session_ssh_hosts: Vec, + hooks_enabled: bool, + http_proxy_enabled: bool, + http_proxy_url: String, + http_proxy_username: String, + http_proxy_password: String, + ui_language: &'static str, + theme: &'static str, + window_scale_percent: u16, + tray_scale_percent: u16, + tray_panel_always_on_top: bool, + powertoys_status_pipe_enabled: bool, + claude_avoid_keychain_prompts: bool, + claude_swap_enabled: bool, + claude_swap_executable_path: String, + codex_spark_usage_visible: bool, + disable_keychain_access: bool, + wayfinder_gateway_url: String, + provider_metrics: std::collections::HashMap, + float_bar_enabled: bool, + float_bar_opacity: u8, + float_bar_scale: u8, + float_bar_orientation: String, + float_bar_style: String, + float_bar_click_through: bool, + float_bar_provider_ids: Vec, + float_bar_dark_text: bool, + float_bar_show_reset_inline: bool, + float_bar_show_cost: bool, + promote_tray_icon: bool, + claude_daily_routines_usage_visible: bool, + claude_allow_reading_claude_code_credentials: bool, + alibaba_token_plan_region: String, + copilot_seat_credit_entitlement: Option, + weekly_progress_work_days: Option, + cost_summary_display_style: &'static str, + open_codex_usage_logs_enabled: bool, + hide_native_codex_cost_when_open_codex_present: bool, + /// History window as its persisted raw form (`rolling:N`, + /// `month-to-date`, `all`). + cost_reporting_period: String, + provider_accent_colors: std::collections::HashMap, +} + +#[tauri::command] +pub fn get_bootstrap_state() -> BootstrapState { + bootstrap_state_for(Settings::load()) +} + +pub(crate) fn bootstrap_state_for(settings: Settings) -> BootstrapState { + BootstrapState { + contract_version: "v1", + providers: provider_catalog_for(&settings), + settings: SettingsSnapshot::from(settings), + } +} + +#[tauri::command] +pub fn get_provider_catalog() -> Vec { + provider_catalog_for(&Settings::load()) +} + +#[tauri::command] +pub fn get_settings_snapshot() -> SettingsSnapshot { + SettingsSnapshot::from(Settings::load()) +} + +impl From for SettingsSnapshot { + fn from(settings: Settings) -> Self { + let avoid_keychain_prompts = settings.claude_avoid_keychain_prompts(); + let claude_swap_enabled = settings.claude_swap_enabled(); + let claude_swap_executable_path = settings.claude_swap_executable_path().to_string(); + let codex_spark_usage_visible = settings.codex_spark_usage_visible(); + let wayfinder_gateway_url = settings.gateway_url(ProviderId::Wayfinder).to_string(); + + let provider_order = settings.provider_display_order_names(); + let enabled_providers = provider_order + .iter() + .filter(|provider_id| settings.enabled_providers.contains(*provider_id)) + .cloned() + .collect(); + + let copilot_seat_credit_entitlement = settings.seat_credit_entitlement(ProviderId::Copilot); + + let provider_metrics = settings + .provider_metrics + .into_iter() + .map(|(k, v)| (k, metric_preference_label(v))) + .collect(); + + Self { + preferred_currency_code: settings.preferred_currency_code, + enabled_providers, + provider_order, + refresh_interval_secs: settings.refresh_interval_secs, + adaptive_refresh: settings.adaptive_refresh, + refresh_all_providers_on_menu_open: settings.refresh_all_providers_on_menu_open, + low_power_mode: settings.low_power_mode_preference + == codexbar::settings::LowPowerModePreference::On, + low_power_mode_preference: settings.low_power_mode_preference.as_str(), + start_at_login: settings.start_at_login, + start_minimized: settings.start_minimized, + show_notifications: settings.show_notifications, + sound_enabled: settings.sound_enabled, + notification_sound_theme: settings.notification_sound_theme, + notification_sound_paths: settings.notification_sound_paths, + high_usage_threshold: settings.high_usage_threshold, + critical_usage_threshold: settings.critical_usage_threshold, + provider_usage_thresholds: settings.provider_usage_thresholds, + predictive_pace_warning_enabled: settings.predictive_pace_warning_enabled, + credential_expiry_notifications_enabled: settings + .credential_expiry_notifications_enabled, + show_pace: settings.show_pace, + tray_icon_mode: tray_icon_mode_label(settings.tray_icon_mode), + stacked_tray_top_provider: settings.stacked_tray_top_provider, + stacked_tray_bottom_provider: settings.stacked_tray_bottom_provider, + switcher_shows_icons: settings.switcher_shows_icons, + menu_bar_shows_highest_usage: settings.menu_bar_shows_highest_usage, + menu_bar_shows_percent: settings.menu_bar_shows_percent, + menu_bar_color_pace: settings.menu_bar_color_pace, + show_as_used: settings.show_as_used, + show_all_token_accounts_in_menu: settings.show_all_token_accounts_in_menu, + enable_animations: settings.enable_animations, + reset_time_relative: settings.reset_time_relative, + show_reset_when_exhausted: settings.show_reset_when_exhausted, + menu_bar_display_mode: settings.menu_bar_display_mode, + overview_layout: settings.overview_layout, + hide_personal_info: settings.hide_personal_info, + update_channel: update_channel_label(settings.update_channel), + auto_download_updates: settings.auto_download_updates, + install_updates_on_quit: settings.install_updates_on_quit, + switcher_shortcuts: codexbar::switcher_shortcuts::resolve_or_default( + &settings.switcher_shortcuts, + ), + global_shortcut: settings.global_shortcut, + codex_custom_sessions_dirs: settings.codex_custom_sessions_dirs, + agent_sessions_enabled: settings.agent_sessions_enabled, + stay_awake_enabled: settings.stay_awake_enabled, + agent_session_ssh_hosts: settings.agent_session_ssh_hosts, + hooks_enabled: settings.hooks_enabled, + http_proxy_enabled: settings.http_proxy_enabled, + http_proxy_url: settings.http_proxy_url, + http_proxy_username: settings.http_proxy_username, + http_proxy_password: settings.http_proxy_password, + ui_language: language_label(settings.ui_language), + theme: theme_label(settings.theme), + window_scale_percent: settings.window_scale_percent, + tray_scale_percent: settings.tray_scale_percent, + tray_panel_always_on_top: settings.tray_panel_always_on_top, + powertoys_status_pipe_enabled: settings.powertoys_status_pipe_enabled, + claude_avoid_keychain_prompts: avoid_keychain_prompts, + claude_swap_enabled, + claude_swap_executable_path, + codex_spark_usage_visible, + disable_keychain_access: settings.disable_keychain_access, + wayfinder_gateway_url, + provider_metrics, + float_bar_enabled: settings.float_bar_enabled, + float_bar_opacity: settings.float_bar_opacity, + float_bar_scale: settings.float_bar_scale, + float_bar_orientation: settings.float_bar_orientation, + float_bar_style: settings.float_bar_style, + float_bar_click_through: settings.float_bar_click_through, + float_bar_provider_ids: settings.float_bar_provider_ids, + float_bar_dark_text: settings.float_bar_dark_text, + float_bar_show_reset_inline: settings.float_bar_show_reset_inline, + float_bar_show_cost: settings.float_bar_show_cost, + promote_tray_icon: settings.promote_tray_icon, + claude_daily_routines_usage_visible: settings.claude_daily_routines_usage_visible, + claude_allow_reading_claude_code_credentials: settings + .claude_allow_reading_claude_code_credentials, + alibaba_token_plan_region: settings.alibaba_token_plan_region, + copilot_seat_credit_entitlement, + weekly_progress_work_days: settings.weekly_progress_work_days, + cost_summary_display_style: cost_summary_display_style_label( + settings.cost_summary_display_style, + ), + open_codex_usage_logs_enabled: settings.open_codex_usage_logs_enabled, + hide_native_codex_cost_when_open_codex_present: settings + .hide_native_codex_cost_when_open_codex_present, + cost_reporting_period: settings.cost_reporting_period.raw(), + provider_accent_colors: settings + .provider_configs + .iter() + .filter_map(|(id, config)| { + config + .accent_color + .as_ref() + .map(|color| (id.cli_name().to_string(), color.clone())) + }) + .collect(), + } + } +} + +pub(crate) fn provider_catalog_for(settings: &Settings) -> Vec { + // Soft-removed providers (upstream #2254) stay hidden in Settings unless already enabled. + settings + .provider_display_order() + .into_iter() + .filter(|provider| settings.is_provider_listed(*provider)) + .map(|provider| ProviderCatalogEntry { + id: provider.cli_name().to_string(), + display_name: provider.display_name().to_string(), + cookie_domain: provider.cookie_domain().map(ToString::to_string), + }) + .collect() +} + +fn tray_icon_mode_label(mode: TrayIconMode) -> &'static str { + match mode { + TrayIconMode::Single => "single", + TrayIconMode::PerProvider => "perProvider", + TrayIconMode::Stacked => "stacked", + } +} + +pub(in crate::commands) fn update_channel_label(channel: UpdateChannel) -> &'static str { + match channel { + UpdateChannel::Stable => "stable", + UpdateChannel::Beta => "beta", + } +} + +pub(in crate::commands) fn language_label(language: Language) -> &'static str { + language.label() +} + +fn theme_label(theme: ThemePreference) -> &'static str { + match theme { + ThemePreference::Auto => "auto", + ThemePreference::Light => "light", + ThemePreference::Dark => "dark", + } +} + +fn cost_summary_display_style_label( + style: codexbar::settings::CostSummaryDisplayStyle, +) -> &'static str { + match style { + codexbar::settings::CostSummaryDisplayStyle::Compact => "compact", + codexbar::settings::CostSummaryDisplayStyle::Detailed => "detailed", + codexbar::settings::CostSummaryDisplayStyle::Hidden => "hidden", + } +} + +pub(crate) fn parse_cost_summary_display_style( + s: &str, +) -> Option { + use codexbar::settings::CostSummaryDisplayStyle; + match s { + "compact" => Some(CostSummaryDisplayStyle::Compact), + "detailed" => Some(CostSummaryDisplayStyle::Detailed), + "hidden" => Some(CostSummaryDisplayStyle::Hidden), + _ => None, + } +} + +pub(in crate::commands) fn parse_theme(s: &str) -> Option { + match s { + "auto" => Some(ThemePreference::Auto), + "light" => Some(ThemePreference::Light), + "dark" => Some(ThemePreference::Dark), + _ => None, + } +} + +fn metric_preference_label(pref: MetricPreference) -> &'static str { + match pref { + MetricPreference::Automatic => "automatic", + MetricPreference::Session => "session", + MetricPreference::Weekly => "weekly", + MetricPreference::Model => "model", + MetricPreference::Tertiary => "tertiary", + MetricPreference::Credits => "credits", + MetricPreference::ExtraUsage => "extraUsage", + MetricPreference::MonthlyPlan => "monthlyPlan", + MetricPreference::Average => "average", + } +} + +pub(in crate::commands) fn parse_metric_preference(s: &str) -> Option { + match s { + "automatic" => Some(MetricPreference::Automatic), + "session" => Some(MetricPreference::Session), + "weekly" => Some(MetricPreference::Weekly), + "model" => Some(MetricPreference::Model), + "tertiary" => Some(MetricPreference::Tertiary), + "credits" => Some(MetricPreference::Credits), + "extraUsage" | "extrausage" => Some(MetricPreference::ExtraUsage), + "monthlyPlan" | "monthlyplan" => Some(MetricPreference::MonthlyPlan), + "average" => Some(MetricPreference::Average), + _ => None, + } +} From 74acd32a99a33e674eb8db49c33bf82ee1e04d99 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:30:49 +0700 Subject: [PATCH 17/22] Reattach the usage-spend tests orphaned by a merge resolution --- apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs b/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs index 2b444263255..8afd818b683 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs @@ -951,3 +951,5 @@ fn cached_spend( #[cfg(test)] mod cache_key_tests; +#[cfg(test)] +mod tests; From dcd1974d0ee1fb8a012839ae3068e266353ed3de Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:34:01 +0700 Subject: [PATCH 18/22] Move the usage-spend summary builder out of commands/usage_spend.rs --- .../src-tauri/src/commands/usage_spend.rs | 551 +----------------- .../src/commands/usage_spend/build.rs | 544 +++++++++++++++++ 2 files changed, 552 insertions(+), 543 deletions(-) create mode 100644 apps/desktop-tauri/src-tauri/src/commands/usage_spend/build.rs diff --git a/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs b/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs index 8afd818b683..594ae51dfb0 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/usage_spend.rs @@ -16,6 +16,14 @@ use crate::state::AppState; use std::collections::{BTreeSet, HashMap}; use std::sync::{Mutex, OnceLock}; +mod build; +use build::build_usage_spend_summary; +#[cfg(test)] +use build::{ + antigravity_spend_values, cached_spend, include_in_shared_overview, period_cost_from_daily, + selected_period_scan, +}; + #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct UsageSpendDailyPoint { @@ -406,549 +414,6 @@ fn usage_spend_cache_key_with_privacy( ) } -fn build_usage_spend_summary( - cached: &[ProviderUsageSnapshot], - period: CostReportingPeriod, - settings: &codexbar::settings::Settings, - force_refresh: bool, -) -> UsageSpendSummary { - let now = Utc::now(); - let period_scan_days = period.scan_days(now); - let include_opencodex = settings.open_codex_usage_logs_enabled; - let hide_native = settings.hide_native_codex_cost_when_open_codex_present; - let pi_selected = settings.enabled_providers.iter().any(|id| id == "pi") - || cached.iter().any(|snapshot| snapshot.provider_id == "pi"); - let include_pi_in_native = !pi_selected; - - // Upstream 0.55.0 #3105: independent provider baselines load in parallel. - // Keep each provider's 7d/30d scans serial so they can safely share that - // provider's incremental cache, while Codex and Claude run concurrently. - let codex_scan_options = if force_refresh { - codexbar::core::CostScanOptions::app_driven() - } else { - codexbar::core::CostScanOptions::default() - }; - let mut codex_scan_options = codex_scan_options; - codex_scan_options.include_pi_sessions = include_pi_in_native; - // Any period other than 7d/30d costs one extra scan per provider. - let ( - (codex_7_summary, codex_30_summary, codex_period_summary), - (claude_7_summary, claude_30_summary, claude_period_summary), - (pi_7_summary, pi_30_summary, pi_period_summary), - ) = std::thread::scope(|scope| { - let codex = scope.spawn(move || { - let seven = CostScanner::new(7) - .with_options(codex_scan_options) - .scan_codex(); - let thirty = CostScanner::new(30) - .with_options(codex_scan_options) - .scan_codex(); - let selected = selected_period_scan(period, &seven, &thirty, || { - CostScanner::for_period(period) - .with_options(codex_scan_options) - .scan_codex() - }); - (seven, thirty, selected) - }); - let claude = scope.spawn(|| { - let seven = CostScanner::new(7) - .scan_claude_with_cancel_and_pi_sessions(None, include_pi_in_native); - let thirty = CostScanner::new(30) - .scan_claude_with_cancel_and_pi_sessions(None, include_pi_in_native); - let selected = selected_period_scan(period, &seven, &thirty, || { - CostScanner::for_period(period) - .scan_claude_with_cancel_and_pi_sessions(None, include_pi_in_native) - }); - (seven, thirty, selected) - }); - let pi = scope.spawn(|| { - let seven = CostScanner::new(7).scan_pi(); - let thirty = CostScanner::new(30).scan_pi(); - let selected = selected_period_scan(period, &seven, &thirty, || { - CostScanner::for_period(period).scan_pi() - }); - (seven, thirty, selected) - }); - ( - codex.join().expect("Codex spend scan worker panicked"), - claude.join().expect("Claude spend scan worker panicked"), - pi.join().expect("Pi spend scan worker panicked"), - ) - }); - - let codex_stale = !codex_30_summary.history_coverage_established; - let codex_stale_updated_at = codex_stale - .then(|| { - codexbar::core::JsonlScanner::load_cache_status(codexbar::core::ProviderId::Codex, None) - .previous_report - .and_then(|report| report.updated_at) - }) - .flatten(); - - let codex_7_contract = build_local_spend_contract_from_summary( - "codex", - 7, - include_opencodex, - hide_native, - settings.hide_personal_info, - codex_7_summary.clone(), - ); - let codex_30_contract = build_local_spend_contract_from_summary( - "codex", - 30, - include_opencodex, - hide_native, - settings.hide_personal_info, - codex_30_summary.clone(), - ); - let codex_period_contract = build_contract_from_period_summary( - "codex", - period, - include_opencodex, - hide_native, - settings.hide_personal_info, - codex_period_summary, - ); - let pi_period_contract = build_contract_from_period_summary( - "pi", - period, - false, - false, - settings.hide_personal_info, - pi_period_summary, - ); - let pi_7_contract = build_local_spend_contract_from_summary( - "pi", - 7, - false, - false, - settings.hide_personal_info, - pi_7_summary.clone(), - ); - let pi_30_contract = build_local_spend_contract_from_summary( - "pi", - 30, - false, - false, - settings.hide_personal_info, - pi_30_summary.clone(), - ); - - let mut provider_ids: BTreeSet = settings.enabled_providers.iter().cloned().collect(); - provider_ids.extend(cached.iter().map(|snapshot| snapshot.provider_id.clone())); - if include_opencodex { - // OpenCodex is an enrichment source, never a standalone provider row. - // Publish routed subscriptions even when no live provider snapshot exists. - for id in ["codex", "opencodego", "kimi", "deepseek", "nous"] { - let contract = match id { - "codex" => None, - _ => Some(build_local_spend_contract(id, 30, true)), - }; - if contract - .as_ref() - .is_some_and(|contract| !contract.imports.is_empty()) - { - provider_ids.insert(id.to_string()); - } - } - } - - let cached_by_id: HashMap<&str, &ProviderUsageSnapshot> = cached - .iter() - .map(|snapshot| (snapshot.provider_id.as_str(), snapshot)) - .collect(); - - let mut rows = Vec::new(); - for provider_id in provider_ids { - let cached_snapshot = cached_by_id.get(provider_id.as_str()).copied(); - let display_name = cached_snapshot - .map(|snapshot| snapshot.display_name.trim()) - .filter(|name| !name.is_empty()) - .map(str::to_string) - .or_else(|| { - codexbar::core::ProviderId::from_cli_name(&provider_id).map(|id| { - codexbar::core::instantiate_provider(id) - .metadata() - .display_name - .to_string() - }) - }) - .unwrap_or_else(|| provider_id.clone()); - - let mut local_cost_estimates = None; - let mut token_lower_bounds = (false, false); - let spend = match provider_id.as_str() { - "codex" => SpendValues { - seven_day: codex_7_contract.known_cost_usd, - thirty_day: codex_30_contract.known_cost_usd, - seven_day_tokens: codex_7_contract.token_total, - thirty_day_tokens: codex_30_contract.token_total, - period_cost: codex_period_contract.known_cost_usd, - period_tokens: codex_period_contract.token_total, - source: if include_opencodex && !codex_30_contract.imports.is_empty() { - "local logs + OpenCodex".to_string() - } else { - "local logs".to_string() - }, - refreshing: codex_stale, - stale_updated_at: codex_stale_updated_at.clone(), - }, - "claude" => SpendValues { - seven_day: Some(claude_7_summary.total_cost_usd), - thirty_day: Some(claude_30_summary.total_cost_usd), - seven_day_tokens: Some(claude_7_summary.total_tokens_for_provider("claude")), - thirty_day_tokens: Some(claude_30_summary.total_tokens_for_provider("claude")), - period_cost: Some(claude_period_summary.total_cost_usd), - period_tokens: Some(claude_period_summary.total_tokens_for_provider("claude")), - source: "local logs".to_string(), - refreshing: false, - stale_updated_at: None, - }, - "pi" => SpendValues { - seven_day: pi_7_contract.known_cost_usd, - thirty_day: pi_30_contract.known_cost_usd, - seven_day_tokens: pi_7_contract.token_total, - thirty_day_tokens: pi_30_contract.token_total, - period_cost: pi_period_contract.known_cost_usd, - period_tokens: pi_period_contract.token_total, - source: "local Pi/OMP history".to_string(), - refreshing: !pi_30_summary.history_coverage_established, - stale_updated_at: None, - }, - "opencodego" | "kimi" | "deepseek" | "nous" if include_opencodex => { - let seven = build_local_spend_contract(&provider_id, 7, true); - let thirty = build_local_spend_contract(&provider_id, 30, true); - if !thirty.imports.is_empty() { - let selected = - build_local_spend_contract_for_period(&provider_id, period, true); - SpendValues { - seven_day: seven.known_cost_usd, - thirty_day: thirty.known_cost_usd, - seven_day_tokens: seven.token_total, - thirty_day_tokens: thirty.token_total, - period_cost: selected.known_cost_usd, - period_tokens: selected.token_total, - source: if provider_id == "opencodego" { - "local logs + OpenCodex".to_string() - } else { - "OpenCodex".to_string() - }, - refreshing: false, - stale_updated_at: None, - } - } else { - cached_spend(cached_snapshot, period, now) - } - } - "opencodego" => { - // Cost stays the provider's; recorded local tokens fill the token columns. - let mut spend = cached_spend(cached_snapshot, period, now); - let seven = CostScanner::new(7).scan_opencodego_usage_tokens_with_cancel(None); - let thirty = CostScanner::new(30).scan_opencodego_usage_tokens_with_cancel(None); - spend.period_tokens = selected_period_scan(period, &seven, &thirty, || { - CostScanner::for_period(period).scan_opencodego_usage_tokens_with_cancel(None) - }); - spend.seven_day_tokens = seven; - spend.thirty_day_tokens = thirty; - spend - } - "cursor" => { - let seven = codexbar::providers::cursor::local_csv::summarize(7); - let thirty = codexbar::providers::cursor::local_csv::summarize(30); - if thirty.row_count > 0 { - let selected = - codexbar::providers::cursor::local_csv::summarize(period_scan_days); - SpendValues { - seven_day: (seven.row_count > 0).then_some(seven.total_cost_usd), - thirty_day: Some(thirty.total_cost_usd), - seven_day_tokens: (seven.row_count > 0).then_some(seven.total_tokens), - thirty_day_tokens: Some(thirty.total_tokens), - period_cost: (selected.row_count > 0).then_some(selected.total_cost_usd), - period_tokens: (selected.row_count > 0).then_some(selected.total_tokens), - source: "local Cursor tokscale cache".to_string(), - refreshing: false, - stale_updated_at: None, - } - } else { - cached_spend(cached_snapshot, period, now) - } - } - "grok" => { - let seven = codexbar::providers::grok::local_sessions::summarize(7); - let thirty = codexbar::providers::grok::local_sessions::summarize(30); - let selected = - codexbar::providers::grok::local_sessions::summarize(period_scan_days); - let mut spend = cached_spend(cached_snapshot, period, now); - spend.seven_day_tokens = (seven.session_count > 0).then_some(seven.total_tokens); - spend.thirty_day_tokens = (thirty.session_count > 0).then_some(thirty.total_tokens); - spend.period_tokens = (selected.session_count > 0).then_some(selected.total_tokens); - if thirty.session_count > 0 { - spend.source = "local Grok sessions".to_string(); - } - spend - } - "antigravity" => { - use codexbar::providers::antigravity::local_sessions; - let seven = local_sessions::summarize(7); - let thirty = local_sessions::summarize(30); - let selected = local_sessions::summarize(period_scan_days); - let mut spend = cached_spend(cached_snapshot, period, now); - // Upstream 0.64: the app refreshes unknown-model pricing in the - // background; a later read (provider refresh or Refresh) reprices. - if let Some(refresh) = local_sessions::background_pricing_refresh(&thirty) { - tauri::async_runtime::spawn(refresh); - } - { - use codexbar::providers::antigravity::local_sessions::LocalHistoryCoverage; - spend.seven_day_tokens = - matches!(seven.coverage, LocalHistoryCoverage::Complete) - .then_some(seven.total_tokens); - spend.thirty_day_tokens = - matches!(thirty.coverage, LocalHistoryCoverage::Complete) - .then_some(thirty.total_tokens); - spend.period_tokens = - matches!(selected.coverage, LocalHistoryCoverage::Complete) - .then_some(selected.total_tokens); - if matches!(thirty.coverage, LocalHistoryCoverage::Complete) { - spend.source = "local Antigravity history".to_string(); - } - } - let spend = antigravity_spend_values(spend, &seven, &thirty); - token_lower_bounds = (seven.lower_bound, thirty.lower_bound); - local_cost_estimates = Some((seven.cost_estimate, thirty.cost_estimate)); - spend - } - _ => cached_spend(cached_snapshot, period, now), - }; - - let currency = cached_snapshot - .and_then(|snapshot| snapshot.cost.as_ref()) - .map(|cost| cost.currency_code.clone()) - .unwrap_or_else(|| "USD".to_string()); - let daily = cached_snapshot - .and_then(|snapshot| snapshot.cost.as_ref()) - .map(|cost| { - cost.daily - .iter() - .map(|point| UsageSpendDailyPoint { - day: point.day.clone(), - amount: point.amount, - }) - .collect() - }) - .unwrap_or_default(); - let (seven_day_estimate, thirty_day_estimate) = local_cost_estimates - .map(|(seven, thirty)| (Some(seven), Some(thirty))) - .unwrap_or((None, None)); - rows.push(UsageSpendRow { - provider_id: provider_id.clone(), - display_name, - seven_day: spend.seven_day, - thirty_day: spend.thirty_day, - seven_day_estimate, - thirty_day_estimate, - seven_day_tokens: spend.seven_day_tokens, - thirty_day_tokens: spend.thirty_day_tokens, - seven_day_tokens_lower_bound: token_lower_bounds.0, - thirty_day_tokens_lower_bound: token_lower_bounds.1, - period_cost: spend.period_cost, - period_tokens: spend.period_tokens, - currency, - source: spend.source, - included_in_overview: include_in_shared_overview( - &provider_id, - settings.enabled_providers.contains(&provider_id), - cached_snapshot.is_some(), - ), - daily, - refreshing: spend.refreshing, - stale_updated_at: spend.stale_updated_at, - }); - } - - let contract = codex_period_contract; - let reporting_day = last_included_reporting_day(&contract); - let dashboard_timezone = codexbar::core::local_timezone_name(); - UsageSpendSummary { - rows, - contract, - reporting_period: period.raw(), - reporting_day, - dashboard_timezone, - } -} - -/// Reuse the fixed scan when it already covers the selected period. -fn selected_period_scan( - period: CostReportingPeriod, - seven: &T, - thirty: &T, - scan: impl FnOnce() -> T, -) -> T { - match period { - CostReportingPeriod::Rolling(7) => seven.clone(), - CostReportingPeriod::Rolling(30) => thirty.clone(), - _ => scan(), - } -} - -/// Pi is an alternate local-history view over rows that may already be -/// projected into Codex or Claude. Keep it out of the shared denominator so -/// enabling Pi cannot double-count the same physical usage. -fn include_in_shared_overview(provider_id: &str, enabled: bool, cached: bool) -> bool { - provider_id != "pi" && (enabled || cached) -} - -fn last_included_reporting_day(contract: &SpendContract) -> String { - contract - .daily - .iter() - .filter_map(|point| chrono::NaiveDate::parse_from_str(&point.day, "%Y-%m-%d").ok()) - .max() - .unwrap_or_else(|| chrono::Local::now().date_naive()) - .format("%Y-%m-%d") - .to_string() -} - -fn antigravity_spend_values( - mut spend: SpendValues, - seven: &codexbar::spend_contract::LocalTokenHistorySummary, - thirty: &codexbar::spend_contract::LocalTokenHistorySummary, -) -> SpendValues { - use codexbar::spend_contract::LocalHistoryCoverage; - - spend.seven_day = seven.total_usd(); - spend.thirty_day = thirty.total_usd(); - // Exact for a complete scan, a floor for a lower bound, unknown otherwise. - spend.seven_day_tokens = seven.published_tokens(); - spend.thirty_day_tokens = thirty.published_tokens(); - if spend.thirty_day.is_some() { - spend.source = "local Antigravity history · API list-price estimate".to_string(); - } else if thirty.cost_estimate.known_subtotal_usd.is_some() { - spend.source = "local Antigravity history · known API list-price subtotal".to_string(); - } else if thirty.coverage == LocalHistoryCoverage::Complete { - spend.source = "local Antigravity history · unpriced".to_string(); - } - spend -} - -/// Provider-reported daily costs bucket by UTC day, so the selected period is -/// resolved in UTC here. `None` when no day falls inside the window. -fn period_cost_from_daily( - daily: &[super::bridge::CostDailyPointBridge], - period: CostReportingPeriod, - now: chrono::DateTime, -) -> Option { - let earliest = daily - .iter() - .filter_map(|point| chrono::NaiveDate::parse_from_str(&point.day, "%Y-%m-%d").ok()) - .min(); - let bounds = period.bounds(now, CostTimeZone::UTC, earliest); - let mut total = 0.0; - let mut saw = false; - for point in daily { - if bounds.contains_day_key(&point.day) { - total += point.amount; - saw = true; - } - } - saw.then_some(total) -} - -fn cached_spend( - snapshot: Option<&ProviderUsageSnapshot>, - reporting_period: CostReportingPeriod, - now: chrono::DateTime, -) -> SpendValues { - let Some(snapshot) = snapshot else { - return SpendValues { - seven_day: None, - thirty_day: None, - seven_day_tokens: None, - thirty_day_tokens: None, - period_cost: None, - period_tokens: None, - source: "unavailable".to_string(), - refreshing: false, - stale_updated_at: None, - }; - }; - let Some(cost) = snapshot.cost.as_ref() else { - return SpendValues { - seven_day: None, - thirty_day: None, - seven_day_tokens: None, - thirty_day_tokens: None, - period_cost: None, - period_tokens: None, - source: if snapshot.error.is_some() { - "unavailable".to_string() - } else { - snapshot.source_label.clone() - }, - refreshing: false, - stale_updated_at: None, - }; - }; - let period = cost.period.trim(); - let period_lower = period.to_ascii_lowercase(); - let (seven_day, thirty_day) = if cost.daily.is_empty() { - ( - None, - (period_lower.contains("30 day") || period_lower.contains("30-day")) - .then_some(cost.used), - ) - } else { - let today = chrono::Utc::now().date_naive(); - let seven_cutoff = today - chrono::Duration::days(6); - let thirty_cutoff = today - chrono::Duration::days(29); - let mut seven = 0.0; - let mut thirty = 0.0; - let mut saw_seven = false; - let mut saw_thirty = false; - for point in &cost.daily { - let Ok(day) = chrono::NaiveDate::parse_from_str(&point.day, "%Y-%m-%d") else { - continue; - }; - // Providers with long daily history (Bedrock keeps 14 months) must - // not widen the fixed 30-day column. - if day > today || day < thirty_cutoff { - continue; - } - thirty += point.amount; - saw_thirty = true; - if day >= seven_cutoff { - seven += point.amount; - saw_seven = true; - } - } - (saw_seven.then_some(seven), saw_thirty.then_some(thirty)) - }; - let period_cost = if cost.daily.is_empty() { - // A provider-reported 30-day total only answers a 30-day selection. - (reporting_period == CostReportingPeriod::Rolling(30)) - .then_some(thirty_day) - .flatten() - } else { - period_cost_from_daily(&cost.daily, reporting_period, now) - }; - SpendValues { - seven_day, - thirty_day, - seven_day_tokens: None, - thirty_day_tokens: None, - period_cost, - period_tokens: None, - source: if period.is_empty() { - snapshot.source_label.clone() - } else { - format!("period ({period})") - }, - refreshing: false, - stale_updated_at: None, - } -} - #[cfg(test)] mod cache_key_tests; #[cfg(test)] diff --git a/apps/desktop-tauri/src-tauri/src/commands/usage_spend/build.rs b/apps/desktop-tauri/src-tauri/src/commands/usage_spend/build.rs new file mode 100644 index 00000000000..a0d8a2f4e43 --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/usage_spend/build.rs @@ -0,0 +1,544 @@ +use super::*; + +pub(super) fn build_usage_spend_summary( + cached: &[ProviderUsageSnapshot], + period: CostReportingPeriod, + settings: &codexbar::settings::Settings, + force_refresh: bool, +) -> UsageSpendSummary { + let now = Utc::now(); + let period_scan_days = period.scan_days(now); + let include_opencodex = settings.open_codex_usage_logs_enabled; + let hide_native = settings.hide_native_codex_cost_when_open_codex_present; + let pi_selected = settings.enabled_providers.iter().any(|id| id == "pi") + || cached.iter().any(|snapshot| snapshot.provider_id == "pi"); + let include_pi_in_native = !pi_selected; + + // Upstream 0.55.0 #3105: independent provider baselines load in parallel. + // Keep each provider's 7d/30d scans serial so they can safely share that + // provider's incremental cache, while Codex and Claude run concurrently. + let codex_scan_options = if force_refresh { + codexbar::core::CostScanOptions::app_driven() + } else { + codexbar::core::CostScanOptions::default() + }; + let mut codex_scan_options = codex_scan_options; + codex_scan_options.include_pi_sessions = include_pi_in_native; + // Any period other than 7d/30d costs one extra scan per provider. + let ( + (codex_7_summary, codex_30_summary, codex_period_summary), + (claude_7_summary, claude_30_summary, claude_period_summary), + (pi_7_summary, pi_30_summary, pi_period_summary), + ) = std::thread::scope(|scope| { + let codex = scope.spawn(move || { + let seven = CostScanner::new(7) + .with_options(codex_scan_options) + .scan_codex(); + let thirty = CostScanner::new(30) + .with_options(codex_scan_options) + .scan_codex(); + let selected = selected_period_scan(period, &seven, &thirty, || { + CostScanner::for_period(period) + .with_options(codex_scan_options) + .scan_codex() + }); + (seven, thirty, selected) + }); + let claude = scope.spawn(|| { + let seven = CostScanner::new(7) + .scan_claude_with_cancel_and_pi_sessions(None, include_pi_in_native); + let thirty = CostScanner::new(30) + .scan_claude_with_cancel_and_pi_sessions(None, include_pi_in_native); + let selected = selected_period_scan(period, &seven, &thirty, || { + CostScanner::for_period(period) + .scan_claude_with_cancel_and_pi_sessions(None, include_pi_in_native) + }); + (seven, thirty, selected) + }); + let pi = scope.spawn(|| { + let seven = CostScanner::new(7).scan_pi(); + let thirty = CostScanner::new(30).scan_pi(); + let selected = selected_period_scan(period, &seven, &thirty, || { + CostScanner::for_period(period).scan_pi() + }); + (seven, thirty, selected) + }); + ( + codex.join().expect("Codex spend scan worker panicked"), + claude.join().expect("Claude spend scan worker panicked"), + pi.join().expect("Pi spend scan worker panicked"), + ) + }); + + let codex_stale = !codex_30_summary.history_coverage_established; + let codex_stale_updated_at = codex_stale + .then(|| { + codexbar::core::JsonlScanner::load_cache_status(codexbar::core::ProviderId::Codex, None) + .previous_report + .and_then(|report| report.updated_at) + }) + .flatten(); + + let codex_7_contract = build_local_spend_contract_from_summary( + "codex", + 7, + include_opencodex, + hide_native, + settings.hide_personal_info, + codex_7_summary.clone(), + ); + let codex_30_contract = build_local_spend_contract_from_summary( + "codex", + 30, + include_opencodex, + hide_native, + settings.hide_personal_info, + codex_30_summary.clone(), + ); + let codex_period_contract = build_contract_from_period_summary( + "codex", + period, + include_opencodex, + hide_native, + settings.hide_personal_info, + codex_period_summary, + ); + let pi_period_contract = build_contract_from_period_summary( + "pi", + period, + false, + false, + settings.hide_personal_info, + pi_period_summary, + ); + let pi_7_contract = build_local_spend_contract_from_summary( + "pi", + 7, + false, + false, + settings.hide_personal_info, + pi_7_summary.clone(), + ); + let pi_30_contract = build_local_spend_contract_from_summary( + "pi", + 30, + false, + false, + settings.hide_personal_info, + pi_30_summary.clone(), + ); + + let mut provider_ids: BTreeSet = settings.enabled_providers.iter().cloned().collect(); + provider_ids.extend(cached.iter().map(|snapshot| snapshot.provider_id.clone())); + if include_opencodex { + // OpenCodex is an enrichment source, never a standalone provider row. + // Publish routed subscriptions even when no live provider snapshot exists. + for id in ["codex", "opencodego", "kimi", "deepseek", "nous"] { + let contract = match id { + "codex" => None, + _ => Some(build_local_spend_contract(id, 30, true)), + }; + if contract + .as_ref() + .is_some_and(|contract| !contract.imports.is_empty()) + { + provider_ids.insert(id.to_string()); + } + } + } + + let cached_by_id: HashMap<&str, &ProviderUsageSnapshot> = cached + .iter() + .map(|snapshot| (snapshot.provider_id.as_str(), snapshot)) + .collect(); + + let mut rows = Vec::new(); + for provider_id in provider_ids { + let cached_snapshot = cached_by_id.get(provider_id.as_str()).copied(); + let display_name = cached_snapshot + .map(|snapshot| snapshot.display_name.trim()) + .filter(|name| !name.is_empty()) + .map(str::to_string) + .or_else(|| { + codexbar::core::ProviderId::from_cli_name(&provider_id).map(|id| { + codexbar::core::instantiate_provider(id) + .metadata() + .display_name + .to_string() + }) + }) + .unwrap_or_else(|| provider_id.clone()); + + let mut local_cost_estimates = None; + let mut token_lower_bounds = (false, false); + let spend = match provider_id.as_str() { + "codex" => SpendValues { + seven_day: codex_7_contract.known_cost_usd, + thirty_day: codex_30_contract.known_cost_usd, + seven_day_tokens: codex_7_contract.token_total, + thirty_day_tokens: codex_30_contract.token_total, + period_cost: codex_period_contract.known_cost_usd, + period_tokens: codex_period_contract.token_total, + source: if include_opencodex && !codex_30_contract.imports.is_empty() { + "local logs + OpenCodex".to_string() + } else { + "local logs".to_string() + }, + refreshing: codex_stale, + stale_updated_at: codex_stale_updated_at.clone(), + }, + "claude" => SpendValues { + seven_day: Some(claude_7_summary.total_cost_usd), + thirty_day: Some(claude_30_summary.total_cost_usd), + seven_day_tokens: Some(claude_7_summary.total_tokens_for_provider("claude")), + thirty_day_tokens: Some(claude_30_summary.total_tokens_for_provider("claude")), + period_cost: Some(claude_period_summary.total_cost_usd), + period_tokens: Some(claude_period_summary.total_tokens_for_provider("claude")), + source: "local logs".to_string(), + refreshing: false, + stale_updated_at: None, + }, + "pi" => SpendValues { + seven_day: pi_7_contract.known_cost_usd, + thirty_day: pi_30_contract.known_cost_usd, + seven_day_tokens: pi_7_contract.token_total, + thirty_day_tokens: pi_30_contract.token_total, + period_cost: pi_period_contract.known_cost_usd, + period_tokens: pi_period_contract.token_total, + source: "local Pi/OMP history".to_string(), + refreshing: !pi_30_summary.history_coverage_established, + stale_updated_at: None, + }, + "opencodego" | "kimi" | "deepseek" | "nous" if include_opencodex => { + let seven = build_local_spend_contract(&provider_id, 7, true); + let thirty = build_local_spend_contract(&provider_id, 30, true); + if !thirty.imports.is_empty() { + let selected = + build_local_spend_contract_for_period(&provider_id, period, true); + SpendValues { + seven_day: seven.known_cost_usd, + thirty_day: thirty.known_cost_usd, + seven_day_tokens: seven.token_total, + thirty_day_tokens: thirty.token_total, + period_cost: selected.known_cost_usd, + period_tokens: selected.token_total, + source: if provider_id == "opencodego" { + "local logs + OpenCodex".to_string() + } else { + "OpenCodex".to_string() + }, + refreshing: false, + stale_updated_at: None, + } + } else { + cached_spend(cached_snapshot, period, now) + } + } + "opencodego" => { + // Cost stays the provider's; recorded local tokens fill the token columns. + let mut spend = cached_spend(cached_snapshot, period, now); + let seven = CostScanner::new(7).scan_opencodego_usage_tokens_with_cancel(None); + let thirty = CostScanner::new(30).scan_opencodego_usage_tokens_with_cancel(None); + spend.period_tokens = selected_period_scan(period, &seven, &thirty, || { + CostScanner::for_period(period).scan_opencodego_usage_tokens_with_cancel(None) + }); + spend.seven_day_tokens = seven; + spend.thirty_day_tokens = thirty; + spend + } + "cursor" => { + let seven = codexbar::providers::cursor::local_csv::summarize(7); + let thirty = codexbar::providers::cursor::local_csv::summarize(30); + if thirty.row_count > 0 { + let selected = + codexbar::providers::cursor::local_csv::summarize(period_scan_days); + SpendValues { + seven_day: (seven.row_count > 0).then_some(seven.total_cost_usd), + thirty_day: Some(thirty.total_cost_usd), + seven_day_tokens: (seven.row_count > 0).then_some(seven.total_tokens), + thirty_day_tokens: Some(thirty.total_tokens), + period_cost: (selected.row_count > 0).then_some(selected.total_cost_usd), + period_tokens: (selected.row_count > 0).then_some(selected.total_tokens), + source: "local Cursor tokscale cache".to_string(), + refreshing: false, + stale_updated_at: None, + } + } else { + cached_spend(cached_snapshot, period, now) + } + } + "grok" => { + let seven = codexbar::providers::grok::local_sessions::summarize(7); + let thirty = codexbar::providers::grok::local_sessions::summarize(30); + let selected = + codexbar::providers::grok::local_sessions::summarize(period_scan_days); + let mut spend = cached_spend(cached_snapshot, period, now); + spend.seven_day_tokens = (seven.session_count > 0).then_some(seven.total_tokens); + spend.thirty_day_tokens = (thirty.session_count > 0).then_some(thirty.total_tokens); + spend.period_tokens = (selected.session_count > 0).then_some(selected.total_tokens); + if thirty.session_count > 0 { + spend.source = "local Grok sessions".to_string(); + } + spend + } + "antigravity" => { + use codexbar::providers::antigravity::local_sessions; + let seven = local_sessions::summarize(7); + let thirty = local_sessions::summarize(30); + let selected = local_sessions::summarize(period_scan_days); + let mut spend = cached_spend(cached_snapshot, period, now); + // Upstream 0.64: the app refreshes unknown-model pricing in the + // background; a later read (provider refresh or Refresh) reprices. + if let Some(refresh) = local_sessions::background_pricing_refresh(&thirty) { + tauri::async_runtime::spawn(refresh); + } + { + use codexbar::providers::antigravity::local_sessions::LocalHistoryCoverage; + spend.seven_day_tokens = + matches!(seven.coverage, LocalHistoryCoverage::Complete) + .then_some(seven.total_tokens); + spend.thirty_day_tokens = + matches!(thirty.coverage, LocalHistoryCoverage::Complete) + .then_some(thirty.total_tokens); + spend.period_tokens = + matches!(selected.coverage, LocalHistoryCoverage::Complete) + .then_some(selected.total_tokens); + if matches!(thirty.coverage, LocalHistoryCoverage::Complete) { + spend.source = "local Antigravity history".to_string(); + } + } + let spend = antigravity_spend_values(spend, &seven, &thirty); + token_lower_bounds = (seven.lower_bound, thirty.lower_bound); + local_cost_estimates = Some((seven.cost_estimate, thirty.cost_estimate)); + spend + } + _ => cached_spend(cached_snapshot, period, now), + }; + + let currency = cached_snapshot + .and_then(|snapshot| snapshot.cost.as_ref()) + .map(|cost| cost.currency_code.clone()) + .unwrap_or_else(|| "USD".to_string()); + let daily = cached_snapshot + .and_then(|snapshot| snapshot.cost.as_ref()) + .map(|cost| { + cost.daily + .iter() + .map(|point| UsageSpendDailyPoint { + day: point.day.clone(), + amount: point.amount, + }) + .collect() + }) + .unwrap_or_default(); + let (seven_day_estimate, thirty_day_estimate) = local_cost_estimates + .map(|(seven, thirty)| (Some(seven), Some(thirty))) + .unwrap_or((None, None)); + rows.push(UsageSpendRow { + provider_id: provider_id.clone(), + display_name, + seven_day: spend.seven_day, + thirty_day: spend.thirty_day, + seven_day_estimate, + thirty_day_estimate, + seven_day_tokens: spend.seven_day_tokens, + thirty_day_tokens: spend.thirty_day_tokens, + seven_day_tokens_lower_bound: token_lower_bounds.0, + thirty_day_tokens_lower_bound: token_lower_bounds.1, + period_cost: spend.period_cost, + period_tokens: spend.period_tokens, + currency, + source: spend.source, + included_in_overview: include_in_shared_overview( + &provider_id, + settings.enabled_providers.contains(&provider_id), + cached_snapshot.is_some(), + ), + daily, + refreshing: spend.refreshing, + stale_updated_at: spend.stale_updated_at, + }); + } + + let contract = codex_period_contract; + let reporting_day = last_included_reporting_day(&contract); + let dashboard_timezone = codexbar::core::local_timezone_name(); + UsageSpendSummary { + rows, + contract, + reporting_period: period.raw(), + reporting_day, + dashboard_timezone, + } +} + +/// Reuse the fixed scan when it already covers the selected period. +pub(super) fn selected_period_scan( + period: CostReportingPeriod, + seven: &T, + thirty: &T, + scan: impl FnOnce() -> T, +) -> T { + match period { + CostReportingPeriod::Rolling(7) => seven.clone(), + CostReportingPeriod::Rolling(30) => thirty.clone(), + _ => scan(), + } +} + +/// Pi is an alternate local-history view over rows that may already be +/// projected into Codex or Claude. Keep it out of the shared denominator so +/// enabling Pi cannot double-count the same physical usage. +pub(super) fn include_in_shared_overview(provider_id: &str, enabled: bool, cached: bool) -> bool { + provider_id != "pi" && (enabled || cached) +} + +fn last_included_reporting_day(contract: &SpendContract) -> String { + contract + .daily + .iter() + .filter_map(|point| chrono::NaiveDate::parse_from_str(&point.day, "%Y-%m-%d").ok()) + .max() + .unwrap_or_else(|| chrono::Local::now().date_naive()) + .format("%Y-%m-%d") + .to_string() +} + +pub(super) fn antigravity_spend_values( + mut spend: SpendValues, + seven: &codexbar::spend_contract::LocalTokenHistorySummary, + thirty: &codexbar::spend_contract::LocalTokenHistorySummary, +) -> SpendValues { + use codexbar::spend_contract::LocalHistoryCoverage; + + spend.seven_day = seven.total_usd(); + spend.thirty_day = thirty.total_usd(); + // Exact for a complete scan, a floor for a lower bound, unknown otherwise. + spend.seven_day_tokens = seven.published_tokens(); + spend.thirty_day_tokens = thirty.published_tokens(); + if spend.thirty_day.is_some() { + spend.source = "local Antigravity history · API list-price estimate".to_string(); + } else if thirty.cost_estimate.known_subtotal_usd.is_some() { + spend.source = "local Antigravity history · known API list-price subtotal".to_string(); + } else if thirty.coverage == LocalHistoryCoverage::Complete { + spend.source = "local Antigravity history · unpriced".to_string(); + } + spend +} + +/// Provider-reported daily costs bucket by UTC day, so the selected period is +/// resolved in UTC here. `None` when no day falls inside the window. +pub(super) fn period_cost_from_daily( + daily: &[crate::commands::bridge::CostDailyPointBridge], + period: CostReportingPeriod, + now: chrono::DateTime, +) -> Option { + let earliest = daily + .iter() + .filter_map(|point| chrono::NaiveDate::parse_from_str(&point.day, "%Y-%m-%d").ok()) + .min(); + let bounds = period.bounds(now, CostTimeZone::UTC, earliest); + let mut total = 0.0; + let mut saw = false; + for point in daily { + if bounds.contains_day_key(&point.day) { + total += point.amount; + saw = true; + } + } + saw.then_some(total) +} + +pub(super) fn cached_spend( + snapshot: Option<&ProviderUsageSnapshot>, + reporting_period: CostReportingPeriod, + now: chrono::DateTime, +) -> SpendValues { + let Some(snapshot) = snapshot else { + return SpendValues { + seven_day: None, + thirty_day: None, + seven_day_tokens: None, + thirty_day_tokens: None, + period_cost: None, + period_tokens: None, + source: "unavailable".to_string(), + refreshing: false, + stale_updated_at: None, + }; + }; + let Some(cost) = snapshot.cost.as_ref() else { + return SpendValues { + seven_day: None, + thirty_day: None, + seven_day_tokens: None, + thirty_day_tokens: None, + period_cost: None, + period_tokens: None, + source: if snapshot.error.is_some() { + "unavailable".to_string() + } else { + snapshot.source_label.clone() + }, + refreshing: false, + stale_updated_at: None, + }; + }; + let period = cost.period.trim(); + let period_lower = period.to_ascii_lowercase(); + let (seven_day, thirty_day) = if cost.daily.is_empty() { + ( + None, + (period_lower.contains("30 day") || period_lower.contains("30-day")) + .then_some(cost.used), + ) + } else { + let today = chrono::Utc::now().date_naive(); + let seven_cutoff = today - chrono::Duration::days(6); + let thirty_cutoff = today - chrono::Duration::days(29); + let mut seven = 0.0; + let mut thirty = 0.0; + let mut saw_seven = false; + let mut saw_thirty = false; + for point in &cost.daily { + let Ok(day) = chrono::NaiveDate::parse_from_str(&point.day, "%Y-%m-%d") else { + continue; + }; + // Providers with long daily history (Bedrock keeps 14 months) must + // not widen the fixed 30-day column. + if day > today || day < thirty_cutoff { + continue; + } + thirty += point.amount; + saw_thirty = true; + if day >= seven_cutoff { + seven += point.amount; + saw_seven = true; + } + } + (saw_seven.then_some(seven), saw_thirty.then_some(thirty)) + }; + let period_cost = if cost.daily.is_empty() { + // A provider-reported 30-day total only answers a 30-day selection. + (reporting_period == CostReportingPeriod::Rolling(30)) + .then_some(thirty_day) + .flatten() + } else { + period_cost_from_daily(&cost.daily, reporting_period, now) + }; + SpendValues { + seven_day, + thirty_day, + seven_day_tokens: None, + thirty_day_tokens: None, + period_cost, + period_tokens: None, + source: if period.is_empty() { + snapshot.source_label.clone() + } else { + format!("period ({period})") + }, + refreshing: false, + stale_updated_at: None, + } +} From 11b2a3a183b14e0b9e0e5e8e4c52e81116e0b2f0 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:37:26 +0700 Subject: [PATCH 19/22] Move the local-usage cache out of commands/chart.rs --- .../src-tauri/src/commands/chart.rs | 209 +---------------- .../src/commands/chart/local_usage_cache.rs | 212 ++++++++++++++++++ 2 files changed, 215 insertions(+), 206 deletions(-) create mode 100644 apps/desktop-tauri/src-tauri/src/commands/chart/local_usage_cache.rs diff --git a/apps/desktop-tauri/src-tauri/src/commands/chart.rs b/apps/desktop-tauri/src-tauri/src/commands/chart.rs index 426406de2ad..0a9875c8cee 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/chart.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/chart.rs @@ -23,6 +23,9 @@ use std::sync::{ }; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +mod local_usage_cache; +pub(crate) use local_usage_cache::*; + const LOCAL_USAGE_TTL: Duration = Duration::from_secs(30); /// A single (date, value) point for cost or credits history charts. @@ -562,212 +565,6 @@ fn muse_local_usage_summary( }) } -pub(crate) fn load_provider_local_usage_summary( - provider_id: &str, -) -> Option { - load_local_usage_summary_cached(provider_id, None) -} - -struct CachedLocalUsage { - loaded_at: Instant, - /// `CostReportingPeriod::identity` the entry was built for; a period or - /// month change makes the entry stale before its TTL. - period_identity: String, - summary: Option, -} - -fn local_usage_period_identity() -> String { - current_reporting_period().identity(Utc::now(), CostTimeZone::Local) -} - -/// Cache a completed scan under the reporting window it actually used. The -/// summary timestamp preserves the month for month-to-date identities. -fn local_usage_summary_period_identity(summary: &ProviderLocalUsageSummary) -> Option { - let period = CostReportingPeriod::parse(&summary.reporting_period)?; - let scanned_at = DateTime::::from_timestamp_millis(summary.token_cost_updated_at_ms)?; - Some(period.identity(scanned_at, CostTimeZone::Local)) -} - -fn local_usage_cache() -> &'static Mutex> { - static CACHE: OnceLock>> = OnceLock::new(); - CACHE.get_or_init(|| Mutex::new(HashMap::new())) -} - -pub(crate) fn clear_provider_local_usage_cache() { - if let Ok(mut guard) = local_usage_cache().lock() { - guard.clear(); - } -} - -pub(crate) fn cached_provider_local_usage_summary( - provider_id: &str, -) -> Option { - let Ok(guard) = local_usage_cache().lock() else { - return None; - }; - guard - .get(provider_id) - .and_then(|entry| entry.summary.clone()) -} - -pub(crate) async fn refresh_provider_local_usage_cache(provider_ids: Vec) { - if provider_ids.is_empty() { - return; - } - - let failure_provider_ids = provider_ids.clone(); - let scans = match tauri::async_runtime::spawn_blocking(move || { - provider_ids - .into_iter() - .map(|provider_id| { - let (summary, unknown_models) = - load_local_usage_summary_with_unknown_models(&provider_id, None); - (provider_id, summary, unknown_models) - }) - .collect::>() - }) - .await - { - Ok(scans) => scans, - Err(err) => { - tracing::warn!("Provider local usage refresh worker failed: {err}"); - for provider_id in failure_provider_ids { - record_local_usage_fetch_failure(&provider_id, CostFetchFailure::Failed); - } - return; - } - }; - - for (provider_id, mut summary, unknown_models) in scans { - let pricing_provider = match provider_id.as_str() { - "codex" => Some("openai"), - "claude" => Some("anthropic"), - _ => None, - }; - if let Some(pricing_provider) = pricing_provider - && codexbar::core::refresh_unknown_models_if_needed(pricing_provider, &unknown_models) - .await - { - let rescan_provider = provider_id.clone(); - summary = tauri::async_runtime::spawn_blocking(move || { - load_local_usage_summary(&rescan_provider, None) - }) - .await - .unwrap_or(summary); - } - store_local_usage_summary(&provider_id, summary); - } -} - -#[cfg(test)] -pub(crate) fn cache_provider_local_usage_summary_for_test( - provider_id: &str, - summary: Option, -) { - store_local_usage_summary(provider_id, summary); -} - -fn load_local_usage_summary_cached( - provider_id: &str, - cancel: Option<&AtomicBool>, -) -> Option { - let cache = local_usage_cache(); - let period_identity = local_usage_period_identity(); - if let Ok(guard) = cache.lock() - && let Some(entry) = guard.get(provider_id) - && entry.period_identity == period_identity - && token_cost_cache_is_fresh(Some(entry.loaded_at), Instant::now(), LOCAL_USAGE_TTL) - { - return entry.summary.clone(); - } - - if cancel.is_some_and(|flag| flag.load(Ordering::Relaxed)) { - return None; - } - - let summary = load_local_usage_summary(provider_id, cancel); - if cancel.is_some_and(|flag| flag.load(Ordering::Relaxed)) { - return None; - } - - store_local_usage_summary(provider_id, summary.clone()); - summary -} - -fn store_local_usage_summary(provider_id: &str, summary: Option) { - let period_identity = match summary.as_ref() { - Some(summary) => { - let Some(period_identity) = local_usage_summary_period_identity(summary) else { - tracing::warn!("Skipping local usage cache entry with invalid period metadata"); - return; - }; - period_identity - } - None => local_usage_period_identity(), - }; - if let Ok(mut guard) = local_usage_cache().lock() { - guard.insert( - provider_id.to_string(), - CachedLocalUsage { - loaded_at: Instant::now(), - period_identity, - summary, - }, - ); - } -} - -fn record_local_usage_fetch_failure(provider_id: &str, failure: CostFetchFailure) { - let loaded_at = if cost_fetch_failure_allows_early_retry(failure) { - Instant::now() - LOCAL_USAGE_TTL - Duration::from_secs(1) - } else { - Instant::now() - }; - let period_identity = local_usage_period_identity(); - if let Ok(mut guard) = local_usage_cache().lock() { - guard.insert( - provider_id.to_string(), - CachedLocalUsage { - loaded_at, - period_identity, - summary: None, - }, - ); - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow( - dead_code, - reason = "chart command helper reserved for future dashboard integration" -)] -pub(crate) enum CostFetchFailure { - Failed, - TimedOut, -} - -pub(crate) fn token_cost_cache_is_fresh( - loaded_at: Option, - now: Instant, - ttl: Duration, -) -> bool { - loaded_at - .and_then(|loaded| now.checked_duration_since(loaded)) - .map(|age| age <= ttl) - .unwrap_or(false) -} - -pub(crate) fn cost_fetch_failure_allows_early_retry(failure: CostFetchFailure) -> bool { - !matches!(failure, CostFetchFailure::TimedOut) -} - -fn current_unix_ms() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis().min(i64::MAX as u128) as i64) - .unwrap_or(0) -} - fn localized_estimate_note(provider_id: &str, lang: codexbar::settings::Language) -> String { match provider_id { "claude" => locale::get_text(lang, LocaleKey::PanelEstimatedFromLocalLogsClaude), diff --git a/apps/desktop-tauri/src-tauri/src/commands/chart/local_usage_cache.rs b/apps/desktop-tauri/src-tauri/src/commands/chart/local_usage_cache.rs new file mode 100644 index 00000000000..6faaa0d6049 --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/chart/local_usage_cache.rs @@ -0,0 +1,212 @@ +use super::*; + +pub(crate) fn load_provider_local_usage_summary( + provider_id: &str, +) -> Option { + load_local_usage_summary_cached(provider_id, None) +} + +pub(super) struct CachedLocalUsage { + loaded_at: Instant, + /// `CostReportingPeriod::identity` the entry was built for; a period or + /// month change makes the entry stale before its TTL. + period_identity: String, + summary: Option, +} + +fn local_usage_period_identity() -> String { + current_reporting_period().identity(Utc::now(), CostTimeZone::Local) +} + +/// Cache a completed scan under the reporting window it actually used. The +/// summary timestamp preserves the month for month-to-date identities. +pub(super) fn local_usage_summary_period_identity( + summary: &ProviderLocalUsageSummary, +) -> Option { + let period = CostReportingPeriod::parse(&summary.reporting_period)?; + let scanned_at = DateTime::::from_timestamp_millis(summary.token_cost_updated_at_ms)?; + Some(period.identity(scanned_at, CostTimeZone::Local)) +} + +pub(super) fn local_usage_cache() -> &'static Mutex> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE.get_or_init(|| Mutex::new(HashMap::new())) +} + +pub(crate) fn clear_provider_local_usage_cache() { + if let Ok(mut guard) = local_usage_cache().lock() { + guard.clear(); + } +} + +pub(crate) fn cached_provider_local_usage_summary( + provider_id: &str, +) -> Option { + let Ok(guard) = local_usage_cache().lock() else { + return None; + }; + guard + .get(provider_id) + .and_then(|entry| entry.summary.clone()) +} + +pub(crate) async fn refresh_provider_local_usage_cache(provider_ids: Vec) { + if provider_ids.is_empty() { + return; + } + + let failure_provider_ids = provider_ids.clone(); + let scans = match tauri::async_runtime::spawn_blocking(move || { + provider_ids + .into_iter() + .map(|provider_id| { + let (summary, unknown_models) = + load_local_usage_summary_with_unknown_models(&provider_id, None); + (provider_id, summary, unknown_models) + }) + .collect::>() + }) + .await + { + Ok(scans) => scans, + Err(err) => { + tracing::warn!("Provider local usage refresh worker failed: {err}"); + for provider_id in failure_provider_ids { + record_local_usage_fetch_failure(&provider_id, CostFetchFailure::Failed); + } + return; + } + }; + + for (provider_id, mut summary, unknown_models) in scans { + let pricing_provider = match provider_id.as_str() { + "codex" => Some("openai"), + "claude" => Some("anthropic"), + _ => None, + }; + if let Some(pricing_provider) = pricing_provider + && codexbar::core::refresh_unknown_models_if_needed(pricing_provider, &unknown_models) + .await + { + let rescan_provider = provider_id.clone(); + summary = tauri::async_runtime::spawn_blocking(move || { + load_local_usage_summary(&rescan_provider, None) + }) + .await + .unwrap_or(summary); + } + store_local_usage_summary(&provider_id, summary); + } +} + +#[cfg(test)] +pub(crate) fn cache_provider_local_usage_summary_for_test( + provider_id: &str, + summary: Option, +) { + store_local_usage_summary(provider_id, summary); +} + +pub(super) fn load_local_usage_summary_cached( + provider_id: &str, + cancel: Option<&AtomicBool>, +) -> Option { + let cache = local_usage_cache(); + let period_identity = local_usage_period_identity(); + if let Ok(guard) = cache.lock() + && let Some(entry) = guard.get(provider_id) + && entry.period_identity == period_identity + && token_cost_cache_is_fresh(Some(entry.loaded_at), Instant::now(), LOCAL_USAGE_TTL) + { + return entry.summary.clone(); + } + + if cancel.is_some_and(|flag| flag.load(Ordering::Relaxed)) { + return None; + } + + let summary = load_local_usage_summary(provider_id, cancel); + if cancel.is_some_and(|flag| flag.load(Ordering::Relaxed)) { + return None; + } + + store_local_usage_summary(provider_id, summary.clone()); + summary +} + +pub(super) fn store_local_usage_summary( + provider_id: &str, + summary: Option, +) { + let period_identity = match summary.as_ref() { + Some(summary) => { + let Some(period_identity) = local_usage_summary_period_identity(summary) else { + tracing::warn!("Skipping local usage cache entry with invalid period metadata"); + return; + }; + period_identity + } + None => local_usage_period_identity(), + }; + if let Ok(mut guard) = local_usage_cache().lock() { + guard.insert( + provider_id.to_string(), + CachedLocalUsage { + loaded_at: Instant::now(), + period_identity, + summary, + }, + ); + } +} + +pub(super) fn record_local_usage_fetch_failure(provider_id: &str, failure: CostFetchFailure) { + let loaded_at = if cost_fetch_failure_allows_early_retry(failure) { + Instant::now() - LOCAL_USAGE_TTL - Duration::from_secs(1) + } else { + Instant::now() + }; + let period_identity = local_usage_period_identity(); + if let Ok(mut guard) = local_usage_cache().lock() { + guard.insert( + provider_id.to_string(), + CachedLocalUsage { + loaded_at, + period_identity, + summary: None, + }, + ); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[allow( + dead_code, + reason = "chart command helper reserved for future dashboard integration" +)] +pub(crate) enum CostFetchFailure { + Failed, + TimedOut, +} + +pub(crate) fn token_cost_cache_is_fresh( + loaded_at: Option, + now: Instant, + ttl: Duration, +) -> bool { + loaded_at + .and_then(|loaded| now.checked_duration_since(loaded)) + .map(|age| age <= ttl) + .unwrap_or(false) +} + +pub(crate) fn cost_fetch_failure_allows_early_retry(failure: CostFetchFailure) -> bool { + !matches!(failure, CostFetchFailure::TimedOut) +} + +pub(super) fn current_unix_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_millis().min(i64::MAX as u128) as i64) + .unwrap_or(0) +} From ffd755a3cfc1bcf574a533b5a1712bd593967186 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:37:26 +0700 Subject: [PATCH 20/22] Move session capture and resume launch out of auto_resume.rs --- .../src-tauri/src/auto_resume.rs | 206 +----------------- .../src/auto_resume/session_target.rs | 204 +++++++++++++++++ 2 files changed, 207 insertions(+), 203 deletions(-) create mode 100644 apps/desktop-tauri/src-tauri/src/auto_resume/session_target.rs diff --git a/apps/desktop-tauri/src-tauri/src/auto_resume.rs b/apps/desktop-tauri/src-tauri/src/auto_resume.rs index 35260c2648b..385412fedba 100644 --- a/apps/desktop-tauri/src-tauri/src/auto_resume.rs +++ b/apps/desktop-tauri/src-tauri/src/auto_resume.rs @@ -18,6 +18,9 @@ use crate::commands::{ProviderUsageSnapshot, RateWindowSnapshot}; use crate::state::AppState; use tauri::Manager; +mod session_target; +use session_target::*; + const MAX_SESSION_ID_LEN: usize = 256; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -459,158 +462,6 @@ fn begin_resume_attempt( Some((target, operation)) } -async fn capture_session( - provider: ProviderId, - token_account_id: Option, - quota_source: ResumeSource, -) -> Option { - // AgentSession has no account identity. A managed token account therefore - // cannot be positively correlated with a discovered process; do not arm - // a session that could belong to another account. - if token_account_id.is_some() { - return None; - } - let result = LocalAgentSessionScanner::default().scan().await; - if result.error.is_some() { - return None; - } - capture_target_from_sessions(provider, &result.sessions, token_account_id, quota_source) -} - -fn capture_target_from_sessions( - provider: ProviderId, - sessions: &[AgentSession], - token_account_id: Option, - quota_source: ResumeSource, -) -> Option { - if token_account_id.is_some() { - return None; - } - let mut candidates = sessions - .iter() - .filter_map(|session| target_from_active_session(provider, session, None, quota_source)); - let target = candidates.next()?; - // Scanner ordering is an activity/display concern, not an account - // correlation. Multiple live candidates are ambiguous, so fail closed. - candidates.next().is_none().then_some(target) -} - -fn target_from_active_session( - provider: ProviderId, - session: &AgentSession, - token_account_id: Option, - quota_source: ResumeSource, -) -> Option { - (session.state == AgentSessionState::Active).then_some(())?; - session.pid?; - target_from_session(provider, session, token_account_id, quota_source) -} - -fn target_from_session( - provider: ProviderId, - session: &AgentSession, - token_account_id: Option, - quota_source: ResumeSource, -) -> Option { - let expected = match provider { - ProviderId::Codex => AgentSessionProvider::Codex, - ProviderId::Claude => AgentSessionProvider::Claude, - _ => return None, - }; - if session.provider != expected - || session.source != AgentSessionSource::Cli - || !is_local_host(&session.host) - { - return None; - } - let session_id = valid_session_id(&session.id)?.to_string(); - let cwd = canonical_workspace(session.workspace.cwd.as_deref())?; - let transcript_path = session - .transcript_path - .as_deref() - .and_then(canonical_transcript); - Some(ResumeTarget { - provider, - quota_source, - session_id, - cwd, - transcript_path, - token_account_id, - }) -} - -fn matching_session<'a>( - target: &ResumeTarget, - sessions: &'a [AgentSession], -) -> Option<&'a AgentSession> { - sessions.iter().find(|session| { - target_from_session( - target.provider, - session, - target.token_account_id, - target.quota_source, - ) - .is_some_and(|candidate| { - candidate.provider == target.provider - && candidate.session_id == target.session_id - && candidate.cwd == target.cwd - }) - }) -} - -fn is_local_host(host: &str) -> bool { - let host = host.trim(); - if host.eq_ignore_ascii_case("localhost") { - return true; - } - std::env::var("COMPUTERNAME") - .ok() - .is_some_and(|local| !local.trim().is_empty() && local.trim().eq_ignore_ascii_case(host)) -} - -fn canonical_workspace(raw: Option<&str>) -> Option { - let raw = raw?.trim(); - if raw.is_empty() { - return None; - } - let path = Path::new(raw); - if !path.is_absolute() { - return None; - } - std::fs::canonicalize(path) - .ok() - .filter(|path| path.is_dir()) -} - -fn canonical_transcript(raw: &str) -> Option { - let path = Path::new(raw.trim()); - path.is_file() - .then(|| std::fs::canonicalize(path).ok()) - .flatten() -} - -fn transcript_is_still_present(target: &ResumeTarget) -> bool { - target.provider == ProviderId::Claude - && target - .transcript_path - .as_ref() - .is_some_and(|path| path.is_file()) -} - -fn valid_session_id(raw: &str) -> Option<&str> { - let id = raw.trim(); - if id.is_empty() - || id.len() > MAX_SESSION_ID_LEN - || id.starts_with("pid:") - || !id - .chars() - .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | ':')) - { - return None; - } - Some(id) -} - async fn resume_captured_session( app: &tauri::AppHandle, target: ResumeTarget, @@ -793,57 +644,6 @@ fn resume_generation_is_current(state: &AppState, provider: ProviderId, generati == generation } -fn launch_resume(target: &ResumeTarget) -> Result<(), String> { - let executable = match target.provider { - ProviderId::Codex => codexbar::codex_cli::locate_codex_binary(), - ProviderId::Claude => codexbar::providers::claude::locate_claude_binary(), - _ => None, - } - .ok_or_else(|| format!("{} CLI was not found", target.provider.display_name()))?; - - let command = build_resume_command(target, executable)?; - // This runs after a background refresh, not a user action: the console - // starts minimized and inactive, so it waits on the taskbar instead of - // taking focus from the app the user is working in. - codexbar::host::console_launch::spawn_minimized_console( - &command.program, - &command.args, - &command.cwd, - ) - .map_err(|error| { - format!( - "failed to launch {} CLI: {error}", - target.provider.display_name() - ) - }) -} - -fn build_resume_command( - target: &ResumeTarget, - executable: PathBuf, -) -> Result { - let session_id = valid_session_id(&target.session_id) - .ok_or_else(|| "captured session id is invalid".to_string())?; - if !target.cwd.is_absolute() { - return Err("captured session workspace must be absolute".to_string()); - } - - let cli_args = match target.provider { - ProviderId::Codex => vec!["resume".to_string(), session_id.to_string()], - ProviderId::Claude => vec!["--resume".to_string(), session_id.to_string()], - _ => return Err("provider does not support auto-resume".to_string()), - }; - - // Batch shims such as npm's `claude.cmd` stay the program here; - // `spawn_minimized_console` runs them through cmd.exe with quoting cmd.exe - // parses. - Ok(ResumeCommand { - program: executable, - args: cli_args, - cwd: target.cwd.clone(), - }) -} - #[cfg(test)] #[path = "auto_resume_tests.rs"] mod auto_resume_tests; diff --git a/apps/desktop-tauri/src-tauri/src/auto_resume/session_target.rs b/apps/desktop-tauri/src-tauri/src/auto_resume/session_target.rs new file mode 100644 index 00000000000..56afde13f4b --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/auto_resume/session_target.rs @@ -0,0 +1,204 @@ +use super::*; + +pub(super) async fn capture_session( + provider: ProviderId, + token_account_id: Option, + quota_source: ResumeSource, +) -> Option { + // AgentSession has no account identity. A managed token account therefore + // cannot be positively correlated with a discovered process; do not arm + // a session that could belong to another account. + if token_account_id.is_some() { + return None; + } + let result = LocalAgentSessionScanner::default().scan().await; + if result.error.is_some() { + return None; + } + capture_target_from_sessions(provider, &result.sessions, token_account_id, quota_source) +} + +pub(super) fn capture_target_from_sessions( + provider: ProviderId, + sessions: &[AgentSession], + token_account_id: Option, + quota_source: ResumeSource, +) -> Option { + if token_account_id.is_some() { + return None; + } + let mut candidates = sessions + .iter() + .filter_map(|session| target_from_active_session(provider, session, None, quota_source)); + let target = candidates.next()?; + // Scanner ordering is an activity/display concern, not an account + // correlation. Multiple live candidates are ambiguous, so fail closed. + candidates.next().is_none().then_some(target) +} + +fn target_from_active_session( + provider: ProviderId, + session: &AgentSession, + token_account_id: Option, + quota_source: ResumeSource, +) -> Option { + (session.state == AgentSessionState::Active).then_some(())?; + session.pid?; + target_from_session(provider, session, token_account_id, quota_source) +} + +fn target_from_session( + provider: ProviderId, + session: &AgentSession, + token_account_id: Option, + quota_source: ResumeSource, +) -> Option { + let expected = match provider { + ProviderId::Codex => AgentSessionProvider::Codex, + ProviderId::Claude => AgentSessionProvider::Claude, + _ => return None, + }; + if session.provider != expected + || session.source != AgentSessionSource::Cli + || !is_local_host(&session.host) + { + return None; + } + let session_id = valid_session_id(&session.id)?.to_string(); + let cwd = canonical_workspace(session.workspace.cwd.as_deref())?; + let transcript_path = session + .transcript_path + .as_deref() + .and_then(canonical_transcript); + Some(ResumeTarget { + provider, + quota_source, + session_id, + cwd, + transcript_path, + token_account_id, + }) +} + +pub(super) fn matching_session<'a>( + target: &ResumeTarget, + sessions: &'a [AgentSession], +) -> Option<&'a AgentSession> { + sessions.iter().find(|session| { + target_from_session( + target.provider, + session, + target.token_account_id, + target.quota_source, + ) + .is_some_and(|candidate| { + candidate.provider == target.provider + && candidate.session_id == target.session_id + && candidate.cwd == target.cwd + }) + }) +} + +fn is_local_host(host: &str) -> bool { + let host = host.trim(); + if host.eq_ignore_ascii_case("localhost") { + return true; + } + std::env::var("COMPUTERNAME") + .ok() + .is_some_and(|local| !local.trim().is_empty() && local.trim().eq_ignore_ascii_case(host)) +} + +fn canonical_workspace(raw: Option<&str>) -> Option { + let raw = raw?.trim(); + if raw.is_empty() { + return None; + } + let path = Path::new(raw); + if !path.is_absolute() { + return None; + } + std::fs::canonicalize(path) + .ok() + .filter(|path| path.is_dir()) +} + +fn canonical_transcript(raw: &str) -> Option { + let path = Path::new(raw.trim()); + path.is_file() + .then(|| std::fs::canonicalize(path).ok()) + .flatten() +} + +pub(super) fn transcript_is_still_present(target: &ResumeTarget) -> bool { + target.provider == ProviderId::Claude + && target + .transcript_path + .as_ref() + .is_some_and(|path| path.is_file()) +} + +fn valid_session_id(raw: &str) -> Option<&str> { + let id = raw.trim(); + if id.is_empty() + || id.len() > MAX_SESSION_ID_LEN + || id.starts_with("pid:") + || !id + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.' | ':')) + { + return None; + } + Some(id) +} + +pub(super) fn launch_resume(target: &ResumeTarget) -> Result<(), String> { + let executable = match target.provider { + ProviderId::Codex => codexbar::codex_cli::locate_codex_binary(), + ProviderId::Claude => codexbar::providers::claude::locate_claude_binary(), + _ => None, + } + .ok_or_else(|| format!("{} CLI was not found", target.provider.display_name()))?; + + let command = build_resume_command(target, executable)?; + // This runs after a background refresh, not a user action: the console + // starts minimized and inactive, so it waits on the taskbar instead of + // taking focus from the app the user is working in. + codexbar::host::console_launch::spawn_minimized_console( + &command.program, + &command.args, + &command.cwd, + ) + .map_err(|error| { + format!( + "failed to launch {} CLI: {error}", + target.provider.display_name() + ) + }) +} + +pub(super) fn build_resume_command( + target: &ResumeTarget, + executable: PathBuf, +) -> Result { + let session_id = valid_session_id(&target.session_id) + .ok_or_else(|| "captured session id is invalid".to_string())?; + if !target.cwd.is_absolute() { + return Err("captured session workspace must be absolute".to_string()); + } + + let cli_args = match target.provider { + ProviderId::Codex => vec!["resume".to_string(), session_id.to_string()], + ProviderId::Claude => vec!["--resume".to_string(), session_id.to_string()], + _ => return Err("provider does not support auto-resume".to_string()), + }; + + // Batch shims such as npm's `claude.cmd` stay the program here; + // `spawn_minimized_console` runs them through cmd.exe with quoting cmd.exe + // parses. + Ok(ResumeCommand { + program: executable, + args: cli_args, + cwd: target.cwd.clone(), + }) +} From 5cec5b1aac7cc02be474fb33f68e32fbe8dfc1f6 Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Sat, 10 Oct 2026 13:41:19 +0700 Subject: [PATCH 21/22] Split commands/tests.rs into provider detail, cache and option submodules --- .../src-tauri/src/commands/tests.rs | 883 +----------------- .../commands/tests/cookie_region_options.rs | 187 ++++ .../src/commands/tests/provider_cache.rs | 445 +++++++++ .../src/commands/tests/provider_detail.rs | 259 +++++ 4 files changed, 895 insertions(+), 879 deletions(-) create mode 100644 apps/desktop-tauri/src-tauri/src/commands/tests/cookie_region_options.rs create mode 100644 apps/desktop-tauri/src-tauri/src/commands/tests/provider_cache.rs create mode 100644 apps/desktop-tauri/src-tauri/src/commands/tests/provider_detail.rs diff --git a/apps/desktop-tauri/src-tauri/src/commands/tests.rs b/apps/desktop-tauri/src-tauri/src/commands/tests.rs index 55be4c2b78e..56d5e288f2e 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/tests.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/tests.rs @@ -12,6 +12,10 @@ use codexbar::core::{ use codexbar::host::session::launch_block_reason; use codexbar::settings::{Language, Settings}; +mod cookie_region_options; +mod provider_cache; +mod provider_detail; + #[test] fn validate_surface_target_accepts_matching_target() { let target = validate_surface_target( @@ -390,699 +394,6 @@ fn launch_block_reason_helper_prefers_ssh() { assert!(msg.contains("SSH")); } -// ── Phase 6b — provider detail pane ──────────────────────────── - -#[test] -fn build_provider_detail_populates_identity_urls() { - let (detail, _settings, _id) = super::build_provider_detail("claude").expect("known provider"); - assert_eq!(detail.id, "claude"); - assert_eq!(detail.display_name, "Claude"); - // Claude advertises a status page URL in its metadata. - assert!(detail.status_page_url.is_some()); - // No snapshot yet — empty usage bars and no error. - assert!(detail.session.is_none()); - assert!(detail.last_error.is_none()); - assert!(!detail.has_snapshot); -} - -#[test] -fn build_provider_detail_rejects_unknown_provider() { - let err = super::build_provider_detail("not-a-provider").unwrap_err(); - assert!(err.contains("not-a-provider")); -} - -#[test] -fn provider_detail_roundtrips_through_serde() { - let (detail, _settings, _id) = super::build_provider_detail("codex").expect("known provider"); - let json = serde_json::to_string(&detail).expect("serialize"); - // camelCase rename survives the round-trip. - assert!(json.contains("\"displayName\"")); - assert!(json.contains("\"hasSnapshot\"")); - assert!(json.contains("\"statusPageUrl\"")); -} - -#[test] -fn provider_detail_carries_openai_daily_usage_only_when_present() { - let (mut detail, _settings, _id) = - super::build_provider_detail("openaiapi").expect("known provider"); - assert!(detail.open_ai_api_usage.is_none()); - let json = serde_json::to_string(&detail).expect("serialize"); - assert!(!json.contains("openAiApiUsage")); - - detail.open_ai_api_usage = Some(super::OpenAiApiUsageSnapshot { - history_days: 30, - project_id: None, - daily: Vec::new(), - }); - let json = serde_json::to_string(&detail).expect("serialize"); - assert!(json.contains("\"openAiApiUsage\":{\"historyDays\":30")); -} - -#[test] -fn usage_item_descriptors_keep_raw_ids_and_redact_titles() { - let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); - let result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), - "OAuth", - ); - let mut snapshot = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); - snapshot.primary_label = Some("Account owner@example.com".to_string()); - snapshot.extra_rate_windows = vec![NamedRateWindowSnapshot { - id: "credits".to_string(), - title: "Credits owner@example.com".to_string(), - window: snapshot.primary.clone(), - fallback_lane: false, - icon_fallback: None, - }]; - - let mut settings = Settings { - hide_personal_info: true, - ..Settings::default() - }; - settings.set_hidden_usage_item_ids(ProviderId::Codex, vec!["metric:extra-missing".to_string()]); - - let items = super::usage_item_descriptors(Some(&snapshot), &settings, ProviderId::Codex); - - assert_eq!(items[0].id, "metric:primary"); - assert_eq!(items[0].title, "Account Hidden"); - assert_eq!(items[1].id, "metric:extra-credits"); - assert_eq!(items[1].title, "Credits Hidden"); - assert_eq!(items[2].id, "metric:extra-missing"); - assert!(!items[2].available); -} - -#[test] -fn usage_item_descriptors_include_detail_sections() { - let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); - let mut result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), - "OAuth", - ); - result.display_details = vec![ - codexbar::core::ProviderDisplayDetail::new("d1", "Rate limits", "5").expect("valid detail"), - codexbar::core::ProviderDisplayDetail::new("d2", "Rate limits", "6").expect("valid detail"), - codexbar::core::ProviderDisplayDetail::new("d3", "owner@example.com quota", "1") - .expect("valid detail"), - ]; - let snapshot = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); - - let mut settings = Settings { - hide_personal_info: true, - ..Settings::default() - }; - settings.set_hidden_usage_item_ids( - ProviderId::Codex, - vec!["detailSection:owner@example.com quota".to_string()], - ); - - let items = super::usage_item_descriptors(Some(&snapshot), &settings, ProviderId::Codex); - - // First three items are the distinct detail sections, redacted titles. - assert_eq!(items[0].id, "detailSection:Rate limits"); - assert_eq!(items[0].title, "Rate limits"); - assert!(items[0].available); - // Duplicate title is deduped to one descriptor; the email title is redacted. - assert!( - !items[1..] - .iter() - .any(|item| item.title.contains("owner@example.com")) - ); - // The hidden detail section is a placeholder, redacted in the stored ID. - let hidden = items - .iter() - .find(|item| item.id.starts_with("detailSection:") && !item.available) - .expect("hidden detail placeholder"); - assert!(hidden.id.contains("owner@example.com quota")); - assert_eq!(hidden.title, "Hidden quota"); -} - -#[test] -fn pace_stage_serializes_to_snake_case_string() { - use codexbar::core::PaceStage; - assert_eq!( - super::bridge::pace::stage_str(PaceStage::OnTrack), - "on_track" - ); - assert_eq!( - super::bridge::pace::stage_str(PaceStage::SlightlyAhead), - "slightly_ahead" - ); - assert_eq!( - super::bridge::pace::stage_str(PaceStage::FarAhead), - "far_ahead" - ); - assert_eq!( - super::bridge::pace::stage_str(PaceStage::SlightlyBehind), - "slightly_behind" - ); - assert_eq!(super::bridge::pace::stage_str(PaceStage::Behind), "behind"); - assert_eq!( - super::bridge::pace::stage_str(PaceStage::FarBehind), - "far_behind" - ); -} - -#[test] -fn local_opencodego_estimates_keep_quota_windows_but_drop_derived_pace() { - let now = chrono::Utc::now(); - let usage = codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::with_details( - 12.0, - Some(300), - Some(now + chrono::Duration::hours(2)), - None, - )) - .with_secondary(codexbar::core::RateWindow::with_details( - 23.0, - Some(10080), - Some(now + chrono::Duration::days(3)), - None, - )) - .with_tertiary(codexbar::core::RateWindow::with_details( - 34.0, - Some(43200), - Some(now + chrono::Duration::days(10)), - None, - )); - let result = ProviderFetchResult::new( - usage, - codexbar::providers::opencodego::LOCAL_ESTIMATE_SOURCE_LABEL, - ) - .with_non_authoritative_pace(); - let metadata = instantiate_provider(ProviderId::OpenCodeGo) - .metadata() - .clone(); - let snapshot = - ProviderUsageSnapshot::from_fetch_result(ProviderId::OpenCodeGo, &metadata, &result, None); - - assert_eq!(snapshot.source_label, "local estimate"); - assert_eq!(snapshot.primary.used_percent, 12.0); - assert_eq!(snapshot.secondary.as_ref().unwrap().used_percent, 23.0); - assert_eq!(snapshot.tertiary.as_ref().unwrap().used_percent, 34.0); - assert!(snapshot.primary.resets_at.is_some()); - assert!(snapshot.secondary.as_ref().unwrap().resets_at.is_some()); - assert!(snapshot.tertiary.as_ref().unwrap().resets_at.is_some()); - assert!(snapshot.pace.is_none()); - assert!( - snapshot - .secondary - .as_ref() - .unwrap() - .reserve_percent - .is_none() - ); -} - -#[test] -fn provider_inventory_maps_to_the_bridge_without_token_ids() { - let expiry = chrono::DateTime::::from_timestamp(1_900_000_000, 0).unwrap(); - let result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(12.0)), - "web", - ) - .with_inventory_item(ProviderInventoryItem { - id: "reset-credits".to_string(), - title: "Limit Reset Credits".to_string(), - available_count: 2, - next_expires_at: Some(expiry), - }) - .with_display_detail( - ProviderDisplayDetail::new("credits", "Used this cycle", "12") - .and_then(|row| row.with_secondary_value("Monthly refill: 100")) - .and_then(|row| row.with_section_title("Credit usage")) - .and_then(|row| row.with_progress(12.0, 100.0)), - ); - let metadata = instantiate_provider(ProviderId::Grok).metadata().clone(); - let snapshot = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Grok, &metadata, &result, None); - - assert_eq!(snapshot.inventory.len(), 1); - assert_eq!(snapshot.inventory[0].available_count, 2); - assert_eq!( - snapshot.inventory[0].next_expires_at.as_deref(), - Some("2030-03-17T17:46:40+00:00") - ); - assert_eq!(snapshot.display_details.len(), 1); - assert_eq!(snapshot.display_details[0].value, "12"); - assert_eq!( - snapshot.display_details[0].section_title.as_deref(), - Some("Credit usage") - ); - let snapshot_json = serde_json::to_value(&snapshot).unwrap(); - assert_eq!( - snapshot_json["displayDetails"][0]["sectionTitle"], - "Credit usage" - ); - assert_eq!( - snapshot.display_details[0].secondary_value.as_deref(), - Some("Monthly refill: 100") - ); - let serialized = serde_json::to_string(&snapshot).unwrap(); - assert!(serialized.contains("reset-credits")); - assert!(!serialized.contains("coupon-token-secret")); -} - -#[test] -fn provider_cache_is_fresh_inside_stale_window() { - assert!(super::is_provider_cache_fresh( - Some(std::time::Instant::now()), - std::time::Duration::from_secs(30), - )); -} - -#[test] -fn provider_cache_is_stale_when_missing_timestamp() { - assert!(!super::is_provider_cache_fresh( - None, - std::time::Duration::from_secs(30), - )); -} - -#[test] -fn provider_cache_is_stale_after_window() { - assert!(!super::is_provider_cache_fresh( - Some(std::time::Instant::now() - std::time::Duration::from_secs(31)), - std::time::Duration::from_secs(30), - )); -} - -#[test] -fn provider_fetch_timeout_allows_slower_authenticated_providers() { - let ctx = FetchContext { - web_timeout: 30, - ..FetchContext::default() - }; - assert_eq!( - super::provider_fetch_timeout(ProviderId::Claude, &ctx), - std::time::Duration::from_secs(75) - ); - assert_eq!( - super::provider_fetch_timeout(ProviderId::Codex, &ctx), - std::time::Duration::from_secs(75) - ); - assert_eq!( - super::provider_fetch_timeout(ProviderId::Copilot, &ctx), - std::time::Duration::from_secs(75) - ); - assert_eq!( - super::provider_fetch_timeout(ProviderId::DeepSeek, &ctx), - std::time::Duration::from_secs(35) - ); - - let optional_litellm_ctx = FetchContext { - web_timeout: 30, - optional_details_enabled: true, - ..FetchContext::default() - }; - assert_eq!( - super::provider_fetch_timeout(ProviderId::LiteLLM, &optional_litellm_ctx), - std::time::Duration::from_secs(40) - ); -} - -#[test] -fn provider_fetch_timeout_respects_context_web_timeout_with_cap() { - let ctx = FetchContext { - web_timeout: 60, - ..FetchContext::default() - }; - assert_eq!( - super::provider_fetch_timeout(ProviderId::T3Chat, &ctx), - std::time::Duration::from_secs(65) - ); - - let ctx = FetchContext { - web_timeout: 120, - ..FetchContext::default() - }; - assert_eq!( - super::provider_fetch_timeout(ProviderId::AzureOpenAI, &ctx), - std::time::Duration::from_secs(65) - ); -} - -#[test] -fn provider_cache_upsert_replaces_existing_provider() { - let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); - let result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), - "CLI", - ); - let mut first = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); - let mut second = first.clone(); - first.error = Some("old".to_string()); - second.error = Some("new".to_string()); - - let mut cache = vec![first]; - super::upsert_provider_cache(&mut cache, second); - - assert_eq!(cache.len(), 1); - assert_eq!(cache[0].provider_id, "codex"); - assert_eq!(cache[0].error.as_deref(), Some("new")); -} - -#[test] -fn provider_cache_prunes_disabled_providers() { - let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); - let result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), - "CLI", - ); - let codex = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); - let claude_meta = instantiate_provider(ProviderId::Claude).metadata().clone(); - let claude = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &claude_meta, &result, None); - - let mut cache = vec![codex, claude]; - super::prune_provider_cache_to_enabled(&mut cache, &[ProviderId::Codex]); - - assert_eq!(cache.len(), 1); - assert_eq!(cache[0].provider_id, "codex"); -} - -#[test] -fn superseded_refresh_generation_is_not_current() { - let mut state = AppState::new(); - state.provider_refresh_generation = 3; - assert!(super::is_current_provider_refresh_generation(&state, 3)); - assert!(!super::is_current_provider_refresh_generation(&state, 2)); -} - -#[test] - -fn claude_transient_auth_failure_preserves_first_last_good_snapshot() { - let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), - "OAuth", - ); - let good = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); - let snapshot = ProviderUsageSnapshot::from_error( - ProviderId::Claude, - &metadata, - "Unauthorized".to_string(), - codexbar::core::ProviderStateKind::NeedsAuthentication, - ); - let error = ProviderError::AuthRequired; - let mut state = crate::state::AppState::new(); - state.provider_cache.push(good.clone()); - - let preserved = super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Claude, - snapshot, - &error, - ); - - assert_eq!(preserved.error, None); - assert_eq!(preserved.primary.used_percent, 42.0); -} - -#[test] -fn codex_transient_transport_failure_helper_uses_typed_policy() { - let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); - let result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), - "OAuth", - ); - let good = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); - let snapshot = ProviderUsageSnapshot::from_error( - ProviderId::Codex, - &metadata, - "Timeout".to_string(), - codexbar::core::ProviderStateKind::Unknown, - ); - let mut state = crate::state::AppState::new(); - state.provider_cache.push(good); - - let preserved = super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Codex, - snapshot, - &ProviderError::Timeout, - ); - - assert_eq!(preserved.error, None); - assert_eq!(preserved.primary.used_percent, 42.0); -} - -#[test] -fn claude_repeated_auth_failure_surfaces_error() { - let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), - "OAuth", - ); - let good = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); - let first_error = ProviderUsageSnapshot::from_error( - ProviderId::Claude, - &metadata, - "Unauthorized".to_string(), - codexbar::core::ProviderStateKind::NeedsAuthentication, - ); - let second_error = first_error.clone(); - let failure = ProviderError::AuthRequired; - let mut state = crate::state::AppState::new(); - state.provider_cache.push(good); - - let _ = super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Claude, - first_error, - &failure, - ); - let surfaced = super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Claude, - second_error, - &failure, - ); - - assert!(surfaced.error.is_some()); -} - -#[test] -fn claude_cloudflare_challenge_retains_prior_usage_while_surfaceing_guidance() { - let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), - "OAuth", - ); - let good = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); - let challenge = codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE; - let error = ProviderUsageSnapshot::from_error( - ProviderId::Claude, - &metadata, - challenge.to_string(), - codexbar::core::ProviderStateKind::Unknown, - ); - let failure = ProviderError::Other(challenge.to_string()); - let mut state = crate::state::AppState::new(); - state.provider_cache.push(good); - - let surfaced = super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Claude, - error, - &failure, - ); - - assert_eq!(surfaced.error, None); - assert_eq!(surfaced.primary.used_percent, 42.0); - assert_eq!( - super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Claude, - ProviderUsageSnapshot::from_error( - ProviderId::Claude, - &metadata, - challenge.to_string(), - codexbar::core::ProviderStateKind::Unknown, - ), - &failure, - ) - .error - .as_deref(), - Some(challenge) - ); -} - -#[test] -fn claude_cloudflare_challenge_keeps_prior_usage_when_guidance_surfaces() { - let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), - "Web", - ); - let mut good = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); - good.updated_at = "2026-09-01T00:00:00Z".to_string(); - let error = ProviderUsageSnapshot::from_error( - ProviderId::Claude, - &metadata, - codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE.to_string(), - codexbar::core::ProviderStateKind::Unknown, - ); - let failure = - ProviderError::Other(codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE.to_string()); - let mut state = crate::state::AppState::new(); - state.provider_cache.push(good.clone()); - - let first = super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Claude, - error.clone(), - &failure, - ); - let second = super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Claude, - error, - &failure, - ); - - assert_eq!(first.error, None); - assert_eq!(first.primary.used_percent, 42.0); - assert_eq!( - second.error.as_deref(), - Some(codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE,) - ); - assert_eq!(second.primary.used_percent, 42.0); - assert_eq!(second.updated_at, good.updated_at); -} - -#[test] -fn claude_cli_parse_failure_keeps_last_good_every_time() { - let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let mut result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(17.0)), - "CLI", - ); - result.has_successful_claude_cli_quota = true; - let good = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); - let err = ProviderUsageSnapshot::from_error( - ProviderId::Claude, - &metadata, - "Parse error: Empty output from Claude CLI".to_string(), - codexbar::core::ProviderStateKind::Unknown, - ); - let failure = ProviderError::Parse("Empty output from Claude CLI".to_string()); - let mut state = crate::state::AppState::new(); - state.provider_cache.push(good.clone()); - - let first = super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Claude, - err.clone(), - &failure, - ); - let second = super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Claude, - err, - &failure, - ); - - assert_eq!(first.error, None); - assert_eq!(first.primary.used_percent, 17.0); - assert!(!first.has_successful_claude_cli_quota); - // Parse failures keep last-good on every refresh (upstream #2247), unlike one-shot auth. - assert_eq!(second.error, None); - assert_eq!(second.primary.used_percent, 17.0); -} - -#[test] -fn claude_hard_credentials_missing_does_not_preserve_stale() { - let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); - let result = ProviderFetchResult::new( - codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(17.0)), - "OAuth", - ); - let good = - ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); - let err = ProviderUsageSnapshot::from_error( - ProviderId::Claude, - &metadata, - "OAuth error: Claude OAuth credentials not found. Run `claude` to authenticate." - .to_string(), - codexbar::core::ProviderStateKind::NeedsAuthentication, - ); - let failure = ProviderError::OAuth( - "Claude OAuth credentials not found. Run `claude` to authenticate.".to_string(), - ); - let mut state = crate::state::AppState::new(); - state.provider_cache.push(good); - - let out = super::providers::preserve_last_good_transient_failure( - &mut state, - ProviderId::Claude, - err, - &failure, - ); - assert!(out.error.is_some()); - assert_eq!( - out.error_state, - codexbar::core::ProviderStateKind::NeedsAuthentication, - "hard auth failure must carry its classification on the snapshot" - ); -} - -#[test] -fn claude_error_message_removes_upstream_swift_cancellation() { - let message = super::friendly_provider_error( - ProviderId::Claude, - "The operation couldn't be completed. (Swift.CancellationError error 1.)", - ); - - assert!(!message.contains("Swift")); - assert!(message.contains("Claude usage fetch was cancelled")); - assert!(message.contains("Refresh Claude")); -} - -#[test] -fn claude_error_message_explains_missing_sign_in() { - let message = super::friendly_provider_error( - ProviderId::Claude, - "OAuth error: Claude OAuth credentials not found. Run `claude` to authenticate.", - ); - - assert_eq!( - message, - "Claude sign-in was not found. Run `claude` once to authenticate, then refresh Claude in Win-CodexBar." - ); -} - -#[test] -fn claude_cloudflare_error_preserves_distinct_recovery_guidance() { - let challenge = codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE; - let message = super::friendly_provider_error(ProviderId::Claude, challenge); - - assert_eq!(message, challenge); - assert!(message.contains("OAuth")); - assert!(message.contains("different network")); -} - -#[test] -fn non_claude_error_message_is_preserved() { - let message = super::friendly_provider_error( - ProviderId::Codex, - "OAuth error: Claude OAuth credentials not found. Run `claude` to authenticate.", - ); - - assert_eq!( - message, - "OAuth error: Claude OAuth credentials not found. Run `claude` to authenticate." - ); -} - #[test] fn chart_data_serde_roundtrip_preserves_fields() { use super::{ @@ -1246,192 +557,6 @@ fn chart_data_requires_account_email_for_codex() { assert!(usage_breakdown.is_empty()); } -#[test] -fn cookie_options_for_cookie_supporting_provider() { - let opts = super::cookie_source_options_for("codex", Language::English); - let values: Vec<_> = opts.iter().map(|o| o.value.as_str()).collect(); - assert_eq!(values, vec!["auto", "manual", "off"]); - assert!(opts.iter().any(|o| o.label == "Automatic")); - assert!(opts.iter().any(|o| o.label == "Manual")); - assert!(opts.iter().any(|o| o.label == "Disabled")); -} - -#[test] -fn replicate_cookie_options_allow_automatic_and_manual_sessions() { - let opts = super::cookie_source_options_for("replicate", Language::English); - let values: Vec<_> = opts.iter().map(|option| option.value.as_str()).collect(); - assert_eq!(values, vec!["auto", "manual"]); -} - -#[test] -fn raycast_cookie_options_include_off_and_a_pinned_manual_session() { - let opts = super::cookie_source_options_for("raycast", Language::English); - let values: Vec<_> = opts.iter().map(|option| option.value.as_str()).collect(); - assert_eq!(values, vec!["auto", "manual", "off"]); -} - -#[test] -fn cookie_options_empty_for_providers_without_picker() { - assert!(super::cookie_source_options_for("anthropic", Language::English).is_empty()); - assert!(super::cookie_source_options_for("unknown", Language::English).is_empty()); -} - -#[test] -fn region_options_for_regional_provider() { - let opts = super::region_options_for("alibaba", Language::English); - let values: Vec<_> = opts.iter().map(|o| o.value.as_str()).collect(); - assert_eq!(values, vec!["singapore", "us", "germany", "hongkong", "cn"]); -} - -#[test] -fn alibaba_token_plan_region_options() { - let opts = super::region_options_for("alibabatokenplan", Language::English); - let values: Vec<_> = opts.iter().map(|o| o.value.as_str()).collect(); - let labels: Vec<_> = opts.iter().map(|o| o.label.as_str()).collect(); - assert_eq!(values, vec!["cn", "intl", "cn-personal", "intl-personal"]); - assert_eq!( - labels, - vec![ - "China Team", - "International Team", - "China Personal/Solo", - "International Personal/Solo" - ] - ); -} - -#[test] -fn minimax_region_options_match_upstream_hosts() { - let opts = super::region_options_for("minimax", Language::English); - let values: Vec<_> = opts.iter().map(|o| o.value.as_str()).collect(); - let labels: Vec<_> = opts.iter().map(|o| o.label.as_str()).collect(); - assert_eq!(values, vec!["global", "cn"]); - assert_eq!( - labels, - vec![ - "Global (platform.minimax.io)", - "China mainland (platform.minimaxi.com)" - ] - ); -} - -#[test] -fn kimi_region_options_match_regional_hosts() { - let opts = super::region_options_for("kimi", Language::English); - let values: Vec<_> = opts.iter().map(|option| option.value.as_str()).collect(); - assert_eq!(values, vec!["china", "international"]); -} - -#[test] -fn cookie_and_region_options_are_localized_for_every_provider() { - // Every description and region label must come from the locale catalog: none may stay - // English in another language, except the labels in ALLOWED_SAME_AS_ENGLISH, which the - // catalogs legitimately keep identical to the English text (proper names). - const ALLOWED_SAME_AS_ENGLISH: &[(Language, &str, &str)] = &[ - (Language::Spanish, "kimi", "china"), - (Language::Spanish, "minimax", "global"), - (Language::Spanish, "zai", "global"), - (Language::PortugueseBrazil, "kimi", "china"), - (Language::PortugueseBrazil, "minimax", "global"), - (Language::PortugueseBrazil, "zai", "global"), - ]; - for &lang in Language::all() { - if lang == Language::English { - continue; - } - for provider in codexbar::core::ProviderId::all() { - let id = provider.cli_name(); - let en = super::cookie_source_options_for(id, Language::English); - let other = super::cookie_source_options_for(id, lang); - assert_eq!(en.len(), other.len(), "{lang:?} {id}"); - for (en, other) in en.iter().zip(&other) { - if let Some(text) = &en.description { - assert_ne!( - Some(text), - other.description.as_ref(), - "{lang:?} {id} {} description", - en.value - ); - } - } - let en = super::region_options_for(id, Language::English); - let other = super::region_options_for(id, lang); - assert_eq!(en.len(), other.len(), "{lang:?} {id} regions"); - for (en, other) in en.iter().zip(&other) { - if ALLOWED_SAME_AS_ENGLISH.contains(&(lang, id, en.value.as_str())) { - continue; - } - assert_ne!(en.label, other.label, "{lang:?} {id} region {}", en.value); - } - } - } -} - -#[test] -fn english_region_labels_match_the_provider_display_names() { - use codexbar::providers::{AlibabaRegion, AlibabaTokenPlanRegion, KimiRegion, MiniMaxRegion}; - let labels = |id| { - super::region_options_for(id, Language::English) - .into_iter() - .map(|option| option.label) - .collect::>() - }; - assert_eq!( - labels("alibaba"), - AlibabaRegion::ALL - .iter() - .map(|r| r.display_name()) - .collect::>() - ); - assert_eq!( - labels("alibabatokenplan"), - AlibabaTokenPlanRegion::ALL - .iter() - .map(|r| r.display_name()) - .collect::>() - ); - assert_eq!( - labels("kimi"), - KimiRegion::ALL - .iter() - .map(|r| r.display_name()) - .collect::>() - ); - assert_eq!( - labels("minimax"), - [MiniMaxRegion::Global, MiniMaxRegion::ChinaMainland].map(|r| r.display_name()) - ); -} - -#[test] -fn region_options_empty_for_non_regional_provider() { - assert!(super::region_options_for("claude", Language::English).is_empty()); - assert!(super::region_options_for("codex", Language::English).is_empty()); -} - -#[test] -fn cookie_source_option_roundtrips_serde() { - let opt = super::CookieSourceOption { - value: "auto".to_string(), - label: "Automatic".to_string(), - description: Some("Imports browser cookies.".to_string()), - }; - let json = serde_json::to_string(&opt).unwrap(); - let back: super::CookieSourceOption = serde_json::from_str(&json).unwrap(); - assert_eq!(opt, back); -} - -#[test] -fn region_option_roundtrips_serde() { - let opt = super::RegionOption { - value: "intl".to_string(), - label: "International".to_string(), - }; - let json = serde_json::to_string(&opt).unwrap(); - let back: super::RegionOption = serde_json::from_str(&json).unwrap(); - assert_eq!(opt, back); -} - // ── Phase 6d — credential detection UIs ──────────────────────── #[test] diff --git a/apps/desktop-tauri/src-tauri/src/commands/tests/cookie_region_options.rs b/apps/desktop-tauri/src-tauri/src/commands/tests/cookie_region_options.rs new file mode 100644 index 00000000000..f1551bc0b8e --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/tests/cookie_region_options.rs @@ -0,0 +1,187 @@ +use super::*; + +#[test] +fn cookie_options_for_cookie_supporting_provider() { + let opts = crate::commands::cookie_source_options_for("codex", Language::English); + let values: Vec<_> = opts.iter().map(|o| o.value.as_str()).collect(); + assert_eq!(values, vec!["auto", "manual", "off"]); + assert!(opts.iter().any(|o| o.label == "Automatic")); + assert!(opts.iter().any(|o| o.label == "Manual")); + assert!(opts.iter().any(|o| o.label == "Disabled")); +} + +#[test] +fn replicate_cookie_options_allow_automatic_and_manual_sessions() { + let opts = crate::commands::cookie_source_options_for("replicate", Language::English); + let values: Vec<_> = opts.iter().map(|option| option.value.as_str()).collect(); + assert_eq!(values, vec!["auto", "manual"]); +} + +#[test] +fn raycast_cookie_options_include_off_and_a_pinned_manual_session() { + let opts = crate::commands::cookie_source_options_for("raycast", Language::English); + let values: Vec<_> = opts.iter().map(|option| option.value.as_str()).collect(); + assert_eq!(values, vec!["auto", "manual", "off"]); +} + +#[test] +fn cookie_options_empty_for_providers_without_picker() { + assert!(crate::commands::cookie_source_options_for("anthropic", Language::English).is_empty()); + assert!(crate::commands::cookie_source_options_for("unknown", Language::English).is_empty()); +} + +#[test] +fn region_options_for_regional_provider() { + let opts = crate::commands::region_options_for("alibaba", Language::English); + let values: Vec<_> = opts.iter().map(|o| o.value.as_str()).collect(); + assert_eq!(values, vec!["singapore", "us", "germany", "hongkong", "cn"]); +} + +#[test] +fn alibaba_token_plan_region_options() { + let opts = crate::commands::region_options_for("alibabatokenplan", Language::English); + let values: Vec<_> = opts.iter().map(|o| o.value.as_str()).collect(); + let labels: Vec<_> = opts.iter().map(|o| o.label.as_str()).collect(); + assert_eq!(values, vec!["cn", "intl", "cn-personal", "intl-personal"]); + assert_eq!( + labels, + vec![ + "China Team", + "International Team", + "China Personal/Solo", + "International Personal/Solo" + ] + ); +} + +#[test] +fn minimax_region_options_match_upstream_hosts() { + let opts = crate::commands::region_options_for("minimax", Language::English); + let values: Vec<_> = opts.iter().map(|o| o.value.as_str()).collect(); + let labels: Vec<_> = opts.iter().map(|o| o.label.as_str()).collect(); + assert_eq!(values, vec!["global", "cn"]); + assert_eq!( + labels, + vec![ + "Global (platform.minimax.io)", + "China mainland (platform.minimaxi.com)" + ] + ); +} + +#[test] +fn kimi_region_options_match_regional_hosts() { + let opts = crate::commands::region_options_for("kimi", Language::English); + let values: Vec<_> = opts.iter().map(|option| option.value.as_str()).collect(); + assert_eq!(values, vec!["china", "international"]); +} + +#[test] +fn cookie_and_region_options_are_localized_for_every_provider() { + // Every description and region label must come from the locale catalog: none may stay + // English in another language, except the labels in ALLOWED_SAME_AS_ENGLISH, which the + // catalogs legitimately keep identical to the English text (proper names). + const ALLOWED_SAME_AS_ENGLISH: &[(Language, &str, &str)] = &[ + (Language::Spanish, "kimi", "china"), + (Language::Spanish, "minimax", "global"), + (Language::Spanish, "zai", "global"), + (Language::PortugueseBrazil, "kimi", "china"), + (Language::PortugueseBrazil, "minimax", "global"), + (Language::PortugueseBrazil, "zai", "global"), + ]; + for &lang in Language::all() { + if lang == Language::English { + continue; + } + for provider in codexbar::core::ProviderId::all() { + let id = provider.cli_name(); + let en = crate::commands::cookie_source_options_for(id, Language::English); + let other = crate::commands::cookie_source_options_for(id, lang); + assert_eq!(en.len(), other.len(), "{lang:?} {id}"); + for (en, other) in en.iter().zip(&other) { + if let Some(text) = &en.description { + assert_ne!( + Some(text), + other.description.as_ref(), + "{lang:?} {id} {} description", + en.value + ); + } + } + let en = crate::commands::region_options_for(id, Language::English); + let other = crate::commands::region_options_for(id, lang); + assert_eq!(en.len(), other.len(), "{lang:?} {id} regions"); + for (en, other) in en.iter().zip(&other) { + if ALLOWED_SAME_AS_ENGLISH.contains(&(lang, id, en.value.as_str())) { + continue; + } + assert_ne!(en.label, other.label, "{lang:?} {id} region {}", en.value); + } + } + } +} + +#[test] +fn english_region_labels_match_the_provider_display_names() { + use codexbar::providers::{AlibabaRegion, AlibabaTokenPlanRegion, KimiRegion, MiniMaxRegion}; + let labels = |id| { + crate::commands::region_options_for(id, Language::English) + .into_iter() + .map(|option| option.label) + .collect::>() + }; + assert_eq!( + labels("alibaba"), + AlibabaRegion::ALL + .iter() + .map(|r| r.display_name()) + .collect::>() + ); + assert_eq!( + labels("alibabatokenplan"), + AlibabaTokenPlanRegion::ALL + .iter() + .map(|r| r.display_name()) + .collect::>() + ); + assert_eq!( + labels("kimi"), + KimiRegion::ALL + .iter() + .map(|r| r.display_name()) + .collect::>() + ); + assert_eq!( + labels("minimax"), + [MiniMaxRegion::Global, MiniMaxRegion::ChinaMainland].map(|r| r.display_name()) + ); +} + +#[test] +fn region_options_empty_for_non_regional_provider() { + assert!(crate::commands::region_options_for("claude", Language::English).is_empty()); + assert!(crate::commands::region_options_for("codex", Language::English).is_empty()); +} + +#[test] +fn cookie_source_option_roundtrips_serde() { + let opt = crate::commands::CookieSourceOption { + value: "auto".to_string(), + label: "Automatic".to_string(), + description: Some("Imports browser cookies.".to_string()), + }; + let json = serde_json::to_string(&opt).unwrap(); + let back: crate::commands::CookieSourceOption = serde_json::from_str(&json).unwrap(); + assert_eq!(opt, back); +} + +#[test] +fn region_option_roundtrips_serde() { + let opt = crate::commands::RegionOption { + value: "intl".to_string(), + label: "International".to_string(), + }; + let json = serde_json::to_string(&opt).unwrap(); + let back: crate::commands::RegionOption = serde_json::from_str(&json).unwrap(); + assert_eq!(opt, back); +} diff --git a/apps/desktop-tauri/src-tauri/src/commands/tests/provider_cache.rs b/apps/desktop-tauri/src-tauri/src/commands/tests/provider_cache.rs new file mode 100644 index 00000000000..26329ff7422 --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/tests/provider_cache.rs @@ -0,0 +1,445 @@ +use super::*; + +#[test] +fn provider_cache_is_fresh_inside_stale_window() { + assert!(crate::commands::is_provider_cache_fresh( + Some(std::time::Instant::now()), + std::time::Duration::from_secs(30), + )); +} + +#[test] +fn provider_cache_is_stale_when_missing_timestamp() { + assert!(!crate::commands::is_provider_cache_fresh( + None, + std::time::Duration::from_secs(30), + )); +} + +#[test] +fn provider_cache_is_stale_after_window() { + assert!(!crate::commands::is_provider_cache_fresh( + Some(std::time::Instant::now() - std::time::Duration::from_secs(31)), + std::time::Duration::from_secs(30), + )); +} + +#[test] +fn provider_fetch_timeout_allows_slower_authenticated_providers() { + let ctx = FetchContext { + web_timeout: 30, + ..FetchContext::default() + }; + assert_eq!( + crate::commands::provider_fetch_timeout(ProviderId::Claude, &ctx), + std::time::Duration::from_secs(75) + ); + assert_eq!( + crate::commands::provider_fetch_timeout(ProviderId::Codex, &ctx), + std::time::Duration::from_secs(75) + ); + assert_eq!( + crate::commands::provider_fetch_timeout(ProviderId::Copilot, &ctx), + std::time::Duration::from_secs(75) + ); + assert_eq!( + crate::commands::provider_fetch_timeout(ProviderId::DeepSeek, &ctx), + std::time::Duration::from_secs(35) + ); + + let optional_litellm_ctx = FetchContext { + web_timeout: 30, + optional_details_enabled: true, + ..FetchContext::default() + }; + assert_eq!( + crate::commands::provider_fetch_timeout(ProviderId::LiteLLM, &optional_litellm_ctx), + std::time::Duration::from_secs(40) + ); +} + +#[test] +fn provider_fetch_timeout_respects_context_web_timeout_with_cap() { + let ctx = FetchContext { + web_timeout: 60, + ..FetchContext::default() + }; + assert_eq!( + crate::commands::provider_fetch_timeout(ProviderId::T3Chat, &ctx), + std::time::Duration::from_secs(65) + ); + + let ctx = FetchContext { + web_timeout: 120, + ..FetchContext::default() + }; + assert_eq!( + crate::commands::provider_fetch_timeout(ProviderId::AzureOpenAI, &ctx), + std::time::Duration::from_secs(65) + ); +} + +#[test] +fn provider_cache_upsert_replaces_existing_provider() { + let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), + "CLI", + ); + let mut first = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); + let mut second = first.clone(); + first.error = Some("old".to_string()); + second.error = Some("new".to_string()); + + let mut cache = vec![first]; + crate::commands::upsert_provider_cache(&mut cache, second); + + assert_eq!(cache.len(), 1); + assert_eq!(cache[0].provider_id, "codex"); + assert_eq!(cache[0].error.as_deref(), Some("new")); +} + +#[test] +fn provider_cache_prunes_disabled_providers() { + let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), + "CLI", + ); + let codex = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); + let claude_meta = instantiate_provider(ProviderId::Claude).metadata().clone(); + let claude = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &claude_meta, &result, None); + + let mut cache = vec![codex, claude]; + crate::commands::prune_provider_cache_to_enabled(&mut cache, &[ProviderId::Codex]); + + assert_eq!(cache.len(), 1); + assert_eq!(cache[0].provider_id, "codex"); +} + +#[test] +fn superseded_refresh_generation_is_not_current() { + let mut state = AppState::new(); + state.provider_refresh_generation = 3; + assert!(crate::commands::is_current_provider_refresh_generation( + &state, 3 + )); + assert!(!crate::commands::is_current_provider_refresh_generation( + &state, 2 + )); +} + +#[test] + +fn claude_transient_auth_failure_preserves_first_last_good_snapshot() { + let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), + "OAuth", + ); + let good = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); + let snapshot = ProviderUsageSnapshot::from_error( + ProviderId::Claude, + &metadata, + "Unauthorized".to_string(), + codexbar::core::ProviderStateKind::NeedsAuthentication, + ); + let error = ProviderError::AuthRequired; + let mut state = crate::state::AppState::new(); + state.provider_cache.push(good.clone()); + + let preserved = crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Claude, + snapshot, + &error, + ); + + assert_eq!(preserved.error, None); + assert_eq!(preserved.primary.used_percent, 42.0); +} + +#[test] +fn codex_transient_transport_failure_helper_uses_typed_policy() { + let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), + "OAuth", + ); + let good = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); + let snapshot = ProviderUsageSnapshot::from_error( + ProviderId::Codex, + &metadata, + "Timeout".to_string(), + codexbar::core::ProviderStateKind::Unknown, + ); + let mut state = crate::state::AppState::new(); + state.provider_cache.push(good); + + let preserved = crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Codex, + snapshot, + &ProviderError::Timeout, + ); + + assert_eq!(preserved.error, None); + assert_eq!(preserved.primary.used_percent, 42.0); +} + +#[test] +fn claude_repeated_auth_failure_surfaces_error() { + let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), + "OAuth", + ); + let good = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); + let first_error = ProviderUsageSnapshot::from_error( + ProviderId::Claude, + &metadata, + "Unauthorized".to_string(), + codexbar::core::ProviderStateKind::NeedsAuthentication, + ); + let second_error = first_error.clone(); + let failure = ProviderError::AuthRequired; + let mut state = crate::state::AppState::new(); + state.provider_cache.push(good); + + let _ = crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Claude, + first_error, + &failure, + ); + let surfaced = crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Claude, + second_error, + &failure, + ); + + assert!(surfaced.error.is_some()); +} + +#[test] +fn claude_cloudflare_challenge_retains_prior_usage_while_surfaceing_guidance() { + let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), + "OAuth", + ); + let good = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); + let challenge = codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE; + let error = ProviderUsageSnapshot::from_error( + ProviderId::Claude, + &metadata, + challenge.to_string(), + codexbar::core::ProviderStateKind::Unknown, + ); + let failure = ProviderError::Other(challenge.to_string()); + let mut state = crate::state::AppState::new(); + state.provider_cache.push(good); + + let surfaced = crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Claude, + error, + &failure, + ); + + assert_eq!(surfaced.error, None); + assert_eq!(surfaced.primary.used_percent, 42.0); + assert_eq!( + crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Claude, + ProviderUsageSnapshot::from_error( + ProviderId::Claude, + &metadata, + challenge.to_string(), + codexbar::core::ProviderStateKind::Unknown, + ), + &failure, + ) + .error + .as_deref(), + Some(challenge) + ); +} + +#[test] +fn claude_cloudflare_challenge_keeps_prior_usage_when_guidance_surfaces() { + let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(42.0)), + "Web", + ); + let mut good = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); + good.updated_at = "2026-09-01T00:00:00Z".to_string(); + let error = ProviderUsageSnapshot::from_error( + ProviderId::Claude, + &metadata, + codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE.to_string(), + codexbar::core::ProviderStateKind::Unknown, + ); + let failure = + ProviderError::Other(codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE.to_string()); + let mut state = crate::state::AppState::new(); + state.provider_cache.push(good.clone()); + + let first = crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Claude, + error.clone(), + &failure, + ); + let second = crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Claude, + error, + &failure, + ); + + assert_eq!(first.error, None); + assert_eq!(first.primary.used_percent, 42.0); + assert_eq!( + second.error.as_deref(), + Some(codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE,) + ); + assert_eq!(second.primary.used_percent, 42.0); + assert_eq!(second.updated_at, good.updated_at); +} + +#[test] +fn claude_cli_parse_failure_keeps_last_good_every_time() { + let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); + let mut result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(17.0)), + "CLI", + ); + result.has_successful_claude_cli_quota = true; + let good = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); + let err = ProviderUsageSnapshot::from_error( + ProviderId::Claude, + &metadata, + "Parse error: Empty output from Claude CLI".to_string(), + codexbar::core::ProviderStateKind::Unknown, + ); + let failure = ProviderError::Parse("Empty output from Claude CLI".to_string()); + let mut state = crate::state::AppState::new(); + state.provider_cache.push(good.clone()); + + let first = crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Claude, + err.clone(), + &failure, + ); + let second = crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Claude, + err, + &failure, + ); + + assert_eq!(first.error, None); + assert_eq!(first.primary.used_percent, 17.0); + assert!(!first.has_successful_claude_cli_quota); + // Parse failures keep last-good on every refresh (upstream #2247), unlike one-shot auth. + assert_eq!(second.error, None); + assert_eq!(second.primary.used_percent, 17.0); +} + +#[test] +fn claude_hard_credentials_missing_does_not_preserve_stale() { + let metadata = instantiate_provider(ProviderId::Claude).metadata().clone(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(17.0)), + "OAuth", + ); + let good = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Claude, &metadata, &result, None); + let err = ProviderUsageSnapshot::from_error( + ProviderId::Claude, + &metadata, + "OAuth error: Claude OAuth credentials not found. Run `claude` to authenticate." + .to_string(), + codexbar::core::ProviderStateKind::NeedsAuthentication, + ); + let failure = ProviderError::OAuth( + "Claude OAuth credentials not found. Run `claude` to authenticate.".to_string(), + ); + let mut state = crate::state::AppState::new(); + state.provider_cache.push(good); + + let out = crate::commands::providers::preserve_last_good_transient_failure( + &mut state, + ProviderId::Claude, + err, + &failure, + ); + assert!(out.error.is_some()); + assert_eq!( + out.error_state, + codexbar::core::ProviderStateKind::NeedsAuthentication, + "hard auth failure must carry its classification on the snapshot" + ); +} + +#[test] +fn claude_error_message_removes_upstream_swift_cancellation() { + let message = crate::commands::friendly_provider_error( + ProviderId::Claude, + "The operation couldn't be completed. (Swift.CancellationError error 1.)", + ); + + assert!(!message.contains("Swift")); + assert!(message.contains("Claude usage fetch was cancelled")); + assert!(message.contains("Refresh Claude")); +} + +#[test] +fn claude_error_message_explains_missing_sign_in() { + let message = crate::commands::friendly_provider_error( + ProviderId::Claude, + "OAuth error: Claude OAuth credentials not found. Run `claude` to authenticate.", + ); + + assert_eq!( + message, + "Claude sign-in was not found. Run `claude` once to authenticate, then refresh Claude in Win-CodexBar." + ); +} + +#[test] +fn claude_cloudflare_error_preserves_distinct_recovery_guidance() { + let challenge = codexbar::providers::claude::CLOUDFLARE_CHALLENGE_MESSAGE; + let message = crate::commands::friendly_provider_error(ProviderId::Claude, challenge); + + assert_eq!(message, challenge); + assert!(message.contains("OAuth")); + assert!(message.contains("different network")); +} + +#[test] +fn non_claude_error_message_is_preserved() { + let message = crate::commands::friendly_provider_error( + ProviderId::Codex, + "OAuth error: Claude OAuth credentials not found. Run `claude` to authenticate.", + ); + + assert_eq!( + message, + "OAuth error: Claude OAuth credentials not found. Run `claude` to authenticate." + ); +} diff --git a/apps/desktop-tauri/src-tauri/src/commands/tests/provider_detail.rs b/apps/desktop-tauri/src-tauri/src/commands/tests/provider_detail.rs new file mode 100644 index 00000000000..9cf9b4730ab --- /dev/null +++ b/apps/desktop-tauri/src-tauri/src/commands/tests/provider_detail.rs @@ -0,0 +1,259 @@ +use super::*; + +#[test] +fn build_provider_detail_populates_identity_urls() { + let (detail, _settings, _id) = + crate::commands::build_provider_detail("claude").expect("known provider"); + assert_eq!(detail.id, "claude"); + assert_eq!(detail.display_name, "Claude"); + // Claude advertises a status page URL in its metadata. + assert!(detail.status_page_url.is_some()); + // No snapshot yet — empty usage bars and no error. + assert!(detail.session.is_none()); + assert!(detail.last_error.is_none()); + assert!(!detail.has_snapshot); +} + +#[test] +fn build_provider_detail_rejects_unknown_provider() { + let err = crate::commands::build_provider_detail("not-a-provider").unwrap_err(); + assert!(err.contains("not-a-provider")); +} + +#[test] +fn provider_detail_roundtrips_through_serde() { + let (detail, _settings, _id) = + crate::commands::build_provider_detail("codex").expect("known provider"); + let json = serde_json::to_string(&detail).expect("serialize"); + // camelCase rename survives the round-trip. + assert!(json.contains("\"displayName\"")); + assert!(json.contains("\"hasSnapshot\"")); + assert!(json.contains("\"statusPageUrl\"")); +} + +#[test] +fn provider_detail_carries_openai_daily_usage_only_when_present() { + let (mut detail, _settings, _id) = + crate::commands::build_provider_detail("openaiapi").expect("known provider"); + assert!(detail.open_ai_api_usage.is_none()); + let json = serde_json::to_string(&detail).expect("serialize"); + assert!(!json.contains("openAiApiUsage")); + + detail.open_ai_api_usage = Some(crate::commands::OpenAiApiUsageSnapshot { + history_days: 30, + project_id: None, + daily: Vec::new(), + }); + let json = serde_json::to_string(&detail).expect("serialize"); + assert!(json.contains("\"openAiApiUsage\":{\"historyDays\":30")); +} + +#[test] +fn usage_item_descriptors_keep_raw_ids_and_redact_titles() { + let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), + "OAuth", + ); + let mut snapshot = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); + snapshot.primary_label = Some("Account owner@example.com".to_string()); + snapshot.extra_rate_windows = vec![NamedRateWindowSnapshot { + id: "credits".to_string(), + title: "Credits owner@example.com".to_string(), + window: snapshot.primary.clone(), + fallback_lane: false, + icon_fallback: None, + }]; + + let mut settings = Settings { + hide_personal_info: true, + ..Settings::default() + }; + settings.set_hidden_usage_item_ids(ProviderId::Codex, vec!["metric:extra-missing".to_string()]); + + let items = + crate::commands::usage_item_descriptors(Some(&snapshot), &settings, ProviderId::Codex); + + assert_eq!(items[0].id, "metric:primary"); + assert_eq!(items[0].title, "Account Hidden"); + assert_eq!(items[1].id, "metric:extra-credits"); + assert_eq!(items[1].title, "Credits Hidden"); + assert_eq!(items[2].id, "metric:extra-missing"); + assert!(!items[2].available); +} + +#[test] +fn usage_item_descriptors_include_detail_sections() { + let metadata = instantiate_provider(ProviderId::Codex).metadata().clone(); + let mut result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(10.0)), + "OAuth", + ); + result.display_details = vec![ + codexbar::core::ProviderDisplayDetail::new("d1", "Rate limits", "5").expect("valid detail"), + codexbar::core::ProviderDisplayDetail::new("d2", "Rate limits", "6").expect("valid detail"), + codexbar::core::ProviderDisplayDetail::new("d3", "owner@example.com quota", "1") + .expect("valid detail"), + ]; + let snapshot = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Codex, &metadata, &result, None); + + let mut settings = Settings { + hide_personal_info: true, + ..Settings::default() + }; + settings.set_hidden_usage_item_ids( + ProviderId::Codex, + vec!["detailSection:owner@example.com quota".to_string()], + ); + + let items = + crate::commands::usage_item_descriptors(Some(&snapshot), &settings, ProviderId::Codex); + + // First three items are the distinct detail sections, redacted titles. + assert_eq!(items[0].id, "detailSection:Rate limits"); + assert_eq!(items[0].title, "Rate limits"); + assert!(items[0].available); + // Duplicate title is deduped to one descriptor; the email title is redacted. + assert!( + !items[1..] + .iter() + .any(|item| item.title.contains("owner@example.com")) + ); + // The hidden detail section is a placeholder, redacted in the stored ID. + let hidden = items + .iter() + .find(|item| item.id.starts_with("detailSection:") && !item.available) + .expect("hidden detail placeholder"); + assert!(hidden.id.contains("owner@example.com quota")); + assert_eq!(hidden.title, "Hidden quota"); +} + +#[test] +fn pace_stage_serializes_to_snake_case_string() { + use codexbar::core::PaceStage; + assert_eq!( + crate::commands::bridge::pace::stage_str(PaceStage::OnTrack), + "on_track" + ); + assert_eq!( + crate::commands::bridge::pace::stage_str(PaceStage::SlightlyAhead), + "slightly_ahead" + ); + assert_eq!( + crate::commands::bridge::pace::stage_str(PaceStage::FarAhead), + "far_ahead" + ); + assert_eq!( + crate::commands::bridge::pace::stage_str(PaceStage::SlightlyBehind), + "slightly_behind" + ); + assert_eq!( + crate::commands::bridge::pace::stage_str(PaceStage::Behind), + "behind" + ); + assert_eq!( + crate::commands::bridge::pace::stage_str(PaceStage::FarBehind), + "far_behind" + ); +} + +#[test] +fn local_opencodego_estimates_keep_quota_windows_but_drop_derived_pace() { + let now = chrono::Utc::now(); + let usage = codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::with_details( + 12.0, + Some(300), + Some(now + chrono::Duration::hours(2)), + None, + )) + .with_secondary(codexbar::core::RateWindow::with_details( + 23.0, + Some(10080), + Some(now + chrono::Duration::days(3)), + None, + )) + .with_tertiary(codexbar::core::RateWindow::with_details( + 34.0, + Some(43200), + Some(now + chrono::Duration::days(10)), + None, + )); + let result = ProviderFetchResult::new( + usage, + codexbar::providers::opencodego::LOCAL_ESTIMATE_SOURCE_LABEL, + ) + .with_non_authoritative_pace(); + let metadata = instantiate_provider(ProviderId::OpenCodeGo) + .metadata() + .clone(); + let snapshot = + ProviderUsageSnapshot::from_fetch_result(ProviderId::OpenCodeGo, &metadata, &result, None); + + assert_eq!(snapshot.source_label, "local estimate"); + assert_eq!(snapshot.primary.used_percent, 12.0); + assert_eq!(snapshot.secondary.as_ref().unwrap().used_percent, 23.0); + assert_eq!(snapshot.tertiary.as_ref().unwrap().used_percent, 34.0); + assert!(snapshot.primary.resets_at.is_some()); + assert!(snapshot.secondary.as_ref().unwrap().resets_at.is_some()); + assert!(snapshot.tertiary.as_ref().unwrap().resets_at.is_some()); + assert!(snapshot.pace.is_none()); + assert!( + snapshot + .secondary + .as_ref() + .unwrap() + .reserve_percent + .is_none() + ); +} + +#[test] +fn provider_inventory_maps_to_the_bridge_without_token_ids() { + let expiry = chrono::DateTime::::from_timestamp(1_900_000_000, 0).unwrap(); + let result = ProviderFetchResult::new( + codexbar::core::UsageSnapshot::new(codexbar::core::RateWindow::new(12.0)), + "web", + ) + .with_inventory_item(ProviderInventoryItem { + id: "reset-credits".to_string(), + title: "Limit Reset Credits".to_string(), + available_count: 2, + next_expires_at: Some(expiry), + }) + .with_display_detail( + ProviderDisplayDetail::new("credits", "Used this cycle", "12") + .and_then(|row| row.with_secondary_value("Monthly refill: 100")) + .and_then(|row| row.with_section_title("Credit usage")) + .and_then(|row| row.with_progress(12.0, 100.0)), + ); + let metadata = instantiate_provider(ProviderId::Grok).metadata().clone(); + let snapshot = + ProviderUsageSnapshot::from_fetch_result(ProviderId::Grok, &metadata, &result, None); + + assert_eq!(snapshot.inventory.len(), 1); + assert_eq!(snapshot.inventory[0].available_count, 2); + assert_eq!( + snapshot.inventory[0].next_expires_at.as_deref(), + Some("2030-03-17T17:46:40+00:00") + ); + assert_eq!(snapshot.display_details.len(), 1); + assert_eq!(snapshot.display_details[0].value, "12"); + assert_eq!( + snapshot.display_details[0].section_title.as_deref(), + Some("Credit usage") + ); + let snapshot_json = serde_json::to_value(&snapshot).unwrap(); + assert_eq!( + snapshot_json["displayDetails"][0]["sectionTitle"], + "Credit usage" + ); + assert_eq!( + snapshot.display_details[0].secondary_value.as_deref(), + Some("Monthly refill: 100") + ); + let serialized = serde_json::to_string(&snapshot).unwrap(); + assert!(serialized.contains("reset-credits")); + assert!(!serialized.contains("coupon-token-secret")); +} From fdf32fa2f08381deb94d7ad1235dec841c227d4c Mon Sep 17 00:00:00 2001 From: Factory Agent Date: Sun, 18 Oct 2026 05:13:07 +0700 Subject: [PATCH 22/22] Restore error snapshot defaults and drop duplicate cfg --- .../src-tauri/src/commands/bridge.rs | 42 ++++++++++++++++++- .../src-tauri/src/commands/mod.rs | 1 - 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 40f314a38ad..197368ce373 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -331,7 +331,7 @@ pub struct ProviderDisplayDetailSnapshot { } /// A frontend-friendly snapshot of one provider's usage data. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ProviderUsageSnapshot { #[serde(default)] @@ -412,6 +412,46 @@ pub struct ProviderUsageSnapshot { pub quota_burndown: Option, } +impl Default for ProviderUsageSnapshot { + fn default() -> Self { + Self { + tertiary_label: None, + provider_id: String::new(), + display_name: String::new(), + primary: RateWindowSnapshot::default(), + primary_label: None, + secondary: None, + secondary_label: None, + secondary_label_key: None, + model_specific: None, + tertiary: None, + extra_rate_windows: Vec::new(), + inventory: Vec::new(), + display_details: Vec::new(), + cost: None, + plan_name: None, + account_email: None, + account_label: None, + subscription: None, + source_label: String::new(), + has_successful_claude_cli_quota: false, + updated_at: String::new(), + error: None, + error_state: default_error_state(), + pace: None, + pace_authoritative: default_pace_authoritative(), + account_organization: None, + tray_status_label: None, + fetch_duration_ms: None, + wayfinder_usage: None, + open_ai_api_usage: None, + throughput: None, + session_equivalent_forecast: None, + quota_burndown: None, + } + } +} + fn default_display_name() -> String { "Codex".to_string() } diff --git a/apps/desktop-tauri/src-tauri/src/commands/mod.rs b/apps/desktop-tauri/src-tauri/src/commands/mod.rs index d60c86fe4e3..fd8497389ef 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/mod.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/mod.rs @@ -88,7 +88,6 @@ pub(crate) use usage_items::*; #[cfg(test)] mod credential_lane_tests; #[cfg(test)] -#[cfg(test)] mod fetch_context_tests; #[cfg(test)] mod last_good_owner_tests;