|
| 1 | +use std::path::{Path, PathBuf}; |
| 2 | + |
| 3 | +use crate::errors::AppResult; |
| 4 | +use crate::models::{CurrentQuotaResponse, ProfileIndexEntry, ProfilesIndex}; |
| 5 | + |
| 6 | +use super::paths::get_codex_home; |
| 7 | +use super::session_usage::{ |
| 8 | + load_latest_local_quota_snapshot, normalize_quota_summary, LocalQuotaSnapshot, |
| 9 | +}; |
| 10 | + |
| 11 | +pub use crate::shared::profiles_index::{load_profiles_index, load_profiles_snapshot}; |
| 12 | + |
| 13 | +fn select_current_quota( |
| 14 | + entry: &ProfileIndexEntry, |
| 15 | + live_snapshot: Option<&LocalQuotaSnapshot>, |
| 16 | +) -> crate::models::QuotaSummary { |
| 17 | + let stored_updated_at_ms = entry.stored_quota_updated_at_ms.unwrap_or(0); |
| 18 | + |
| 19 | + match live_snapshot { |
| 20 | + Some(snapshot) if snapshot.source_mtime_ms.unwrap_or(0) > stored_updated_at_ms => { |
| 21 | + snapshot.quota.clone() |
| 22 | + } |
| 23 | + _ => entry.stored_quota.clone(), |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +pub fn load_current_live_quota(codex_home: Option<&Path>) -> AppResult<CurrentQuotaResponse> { |
| 28 | + let codex_home = codex_home.map(PathBuf::from).unwrap_or_else(get_codex_home); |
| 29 | + let index: ProfilesIndex = load_profiles_index(Some(&codex_home))?; |
| 30 | + let Some(current_profile) = index.current_profile.clone() else { |
| 31 | + return Ok(CurrentQuotaResponse { |
| 32 | + profile: None, |
| 33 | + quota: None, |
| 34 | + }); |
| 35 | + }; |
| 36 | + let Some(entry) = index |
| 37 | + .profiles |
| 38 | + .iter() |
| 39 | + .find(|profile| profile.folder_name == current_profile) |
| 40 | + else { |
| 41 | + return Ok(CurrentQuotaResponse { |
| 42 | + profile: Some(current_profile), |
| 43 | + quota: None, |
| 44 | + }); |
| 45 | + }; |
| 46 | + |
| 47 | + let live_snapshot = load_latest_local_quota_snapshot(Some(&codex_home)); |
| 48 | + let quota = normalize_quota_summary( |
| 49 | + Some(select_current_quota(entry, live_snapshot.as_ref())), |
| 50 | + entry.plan_name.as_deref(), |
| 51 | + entry.has_account_identity, |
| 52 | + ); |
| 53 | + |
| 54 | + Ok(CurrentQuotaResponse { |
| 55 | + profile: Some(entry.folder_name.clone()), |
| 56 | + quota: Some(quota), |
| 57 | + }) |
| 58 | +} |
| 59 | + |
| 60 | +#[cfg(test)] |
| 61 | +mod tests { |
| 62 | + use std::fs; |
| 63 | + use std::path::PathBuf; |
| 64 | + use std::time::{SystemTime, UNIX_EPOCH}; |
| 65 | + |
| 66 | + use super::load_current_live_quota; |
| 67 | + use crate::windows::env_guard; |
| 68 | + |
| 69 | + fn temp_codex_home(name: &str) -> PathBuf { |
| 70 | + let unique = SystemTime::now() |
| 71 | + .duration_since(UNIX_EPOCH) |
| 72 | + .unwrap() |
| 73 | + .as_nanos(); |
| 74 | + std::env::temp_dir().join(format!("codex-switch-win-profiles-index-{name}-{unique}")) |
| 75 | + } |
| 76 | + |
| 77 | + fn write_quota_session(path: PathBuf, primary_used_percent: f64, secondary_used_percent: f64) { |
| 78 | + let line = format!( |
| 79 | + r#"{{"type":"event_msg","payload":{{"type":"token_count","rate_limits":{{"limit_id":"codex","primary":{{"used_percent":{primary_used_percent},"resets_at":1730000000,"window_minutes":300}},"secondary":{{"used_percent":{secondary_used_percent},"resets_at":1730600000,"window_minutes":10080}}}}}}}}"# |
| 80 | + ); |
| 81 | + if let Some(parent) = path.parent() { |
| 82 | + fs::create_dir_all(parent).unwrap(); |
| 83 | + } |
| 84 | + fs::write(path, format!("{line}\n")).unwrap(); |
| 85 | + } |
| 86 | + |
| 87 | + #[test] |
| 88 | + fn current_live_quota_prefers_stored_target_quota_when_it_was_primed_after_switch() { |
| 89 | + let _guard = env_guard(); |
| 90 | + let codex_home = temp_codex_home("prefer-primed-stored"); |
| 91 | + let profile_dir = codex_home.join("account_backup").join("b"); |
| 92 | + fs::create_dir_all(&profile_dir).unwrap(); |
| 93 | + fs::write( |
| 94 | + codex_home.join("account_backup").join(".current_profile"), |
| 95 | + "b\n", |
| 96 | + ) |
| 97 | + .unwrap(); |
| 98 | + fs::write(profile_dir.join("auth.json"), "profile-b-auth\n").unwrap(); |
| 99 | + fs::write( |
| 100 | + profile_dir.join("profile.json"), |
| 101 | + r#"{"folder_name":"b","account_label":"b@example.com","quota":{"five_hour":{},"weekly":{}},"quota_updated_at_ms":9999999999999}"#, |
| 102 | + ) |
| 103 | + .unwrap(); |
| 104 | + write_quota_session( |
| 105 | + codex_home.join("sessions").join("session-001.jsonl"), |
| 106 | + 11.0, |
| 107 | + 12.0, |
| 108 | + ); |
| 109 | + |
| 110 | + let response = load_current_live_quota(Some(&codex_home)).unwrap(); |
| 111 | + |
| 112 | + assert_eq!(response.profile.as_deref(), Some("b")); |
| 113 | + let quota = response.quota.expect("expected quota"); |
| 114 | + assert_eq!(quota.five_hour.remaining_percent, None); |
| 115 | + assert_eq!(quota.weekly.remaining_percent, None); |
| 116 | + |
| 117 | + let _ = fs::remove_dir_all(&codex_home); |
| 118 | + } |
| 119 | + |
| 120 | + #[test] |
| 121 | + fn current_live_quota_still_uses_live_session_when_it_is_newer_than_stored_quota() { |
| 122 | + let _guard = env_guard(); |
| 123 | + let codex_home = temp_codex_home("prefer-live-when-newer"); |
| 124 | + let profile_dir = codex_home.join("account_backup").join("b"); |
| 125 | + fs::create_dir_all(&profile_dir).unwrap(); |
| 126 | + fs::write( |
| 127 | + codex_home.join("account_backup").join(".current_profile"), |
| 128 | + "b\n", |
| 129 | + ) |
| 130 | + .unwrap(); |
| 131 | + fs::write(profile_dir.join("auth.json"), "profile-b-auth\n").unwrap(); |
| 132 | + fs::write( |
| 133 | + profile_dir.join("profile.json"), |
| 134 | + r#"{"folder_name":"b","account_label":"b@example.com","quota":{"five_hour":{"remaining_percent":41},"weekly":{"remaining_percent":63}},"quota_updated_at_ms":1}"#, |
| 135 | + ) |
| 136 | + .unwrap(); |
| 137 | + write_quota_session( |
| 138 | + codex_home.join("sessions").join("session-001.jsonl"), |
| 139 | + 11.0, |
| 140 | + 12.0, |
| 141 | + ); |
| 142 | + |
| 143 | + let response = load_current_live_quota(Some(&codex_home)).unwrap(); |
| 144 | + |
| 145 | + assert_eq!(response.profile.as_deref(), Some("b")); |
| 146 | + let quota = response.quota.expect("expected quota"); |
| 147 | + assert_eq!(quota.five_hour.remaining_percent, Some(89)); |
| 148 | + assert_eq!(quota.weekly.remaining_percent, Some(88)); |
| 149 | + |
| 150 | + let _ = fs::remove_dir_all(&codex_home); |
| 151 | + } |
| 152 | +} |
0 commit comments