diff --git a/src/admin/monitoring/billing.rs b/src/admin/monitoring/billing.rs index 10200d762..5813efe1f 100644 --- a/src/admin/monitoring/billing.rs +++ b/src/admin/monitoring/billing.rs @@ -382,43 +382,3 @@ fn normalize_service_tier(service_tier: Option<&str>) -> Option { .filter(|value| !value.is_empty()) .map(str::to_ascii_lowercase) } - -#[cfg(test)] -mod tests { - use super::calculate_cost; - - #[test] - fn calculate_cost_should_use_gpt55_standard_prices() { - let actual = calculate_cost(100_000, 100_000, 0, "gpt-5.5", None); - - assert_eq!(actual, 3.5); - } - - #[test] - fn calculate_cost_should_use_priority_prices_when_available() { - let actual = calculate_cost(100_000, 100_000, 0, "gpt-5.5", Some("priority")); - - assert_eq!(actual, 8.75); - } - - #[test] - fn calculate_cost_should_charge_cached_input_at_cache_read_price() { - let actual = calculate_cost(1_000, 1_000, 500, "gpt-5.5", None); - - assert!((actual - 0.03275).abs() < 1e-12); - } - - #[test] - fn calculate_cost_should_apply_long_context_prices() { - let actual = calculate_cost(300_000, 1_000, 0, "gpt-5.5", None); - - assert!((actual - 3.045).abs() < 1e-12); - } - - #[test] - fn calculate_cost_should_normalize_codex_model_names() { - let actual = calculate_cost(100_000, 100_000, 0, "codex-auto-review", None); - - assert_eq!(actual, 1.75); - } -} diff --git a/src/admin/monitoring/dashboard.rs b/src/admin/monitoring/dashboard.rs index 87ef00cfc..f362c2ad7 100644 --- a/src/admin/monitoring/dashboard.rs +++ b/src/admin/monitoring/dashboard.rs @@ -1,6 +1,6 @@ //! 管理端 Dashboard 聚合视图。 -use std::collections::HashMap; +use std::{cmp::Reverse, collections::HashMap}; use axum::{ extract::{Query, State}, @@ -39,20 +39,15 @@ pub struct DashboardTrendQuery { pub kind: Option, } -#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum DashboardTrendKind { + #[default] Usage, Latency, Errors, } -impl Default for DashboardTrendKind { - fn default() -> Self { - Self::Usage - } -} - #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct DashboardSummaryData { @@ -203,19 +198,12 @@ impl UsageWindow { } fn avg_first_token_latency(self) -> Option { - if self.first_token_latency_count > 0 { - Some(self.first_token_latency_sum / self.first_token_latency_count) - } else { - None - } + self.first_token_latency_sum + .checked_div(self.first_token_latency_count) } fn avg_latency(self) -> Option { - if self.latency_count > 0 { - Some(self.latency_sum / self.latency_count) - } else { - None - } + self.latency_sum.checked_div(self.latency_count) } } @@ -752,7 +740,7 @@ fn account_usage_data( } }) .collect::>(); - rows.sort_by(|a, b| b.requests.cmp(&a.requests)); + rows.sort_by_key(|row| Reverse(row.requests)); rows.truncate(4); rows } @@ -762,7 +750,7 @@ async fn account_quota_used_percent_by_id( usage_records: &[AdminUsageRecord], ) -> HashMap { let mut records = usage_records.iter().collect::>(); - records.sort_by(|a, b| b.request_count.cmp(&a.request_count)); + records.sort_by_key(|record| Reverse(record.request_count)); records.truncate(4); let mut quota_used_by_account = HashMap::with_capacity(records.len()); @@ -903,137 +891,3 @@ fn hour_start(value: DateTime) -> DateTime { .expect("valid hour") .and_utc() } - -#[cfg(test)] -mod tests { - use chrono::{TimeZone, Utc}; - use serde_json::json; - - use super::{ - cost_window, dashboard_cards, day_start, empty_usage_summary, format_beijing_datetime, - total_cost, BillingContext, - }; - use crate::admin::monitoring::events::{EventLevel, EventLog}; - - #[test] - fn format_beijing_datetime_should_render_rfc3339_as_beijing_time() { - let actual = - format_beijing_datetime(Some("2026-06-23T11:36:46.965574590+00:00".to_string())); - - assert_eq!(actual, "2026-06-23 19:36:46"); - } - - #[test] - fn format_beijing_datetime_should_render_dash_when_missing() { - assert_eq!(format_beijing_datetime(None), "-"); - } - - #[test] - fn cost_window_should_calculate_from_event_model_and_service_tier() { - let timestamp = Utc.with_ymd_and_hms(2026, 6, 24, 8, 0, 0).unwrap(); - let logs = vec![usage_log( - timestamp, - "gpt-5.5", - json!({ - "usage": { - "inputTokens": 100_000u64, - "outputTokens": 100_000u64, - "cachedTokens": 0u64 - }, - "serviceTier": "priority" - }), - )]; - - let actual = cost_window( - &logs, - timestamp - chrono::Duration::hours(1), - timestamp + chrono::Duration::hours(1), - BillingContext { - default_model: "gpt-4o", - default_service_tier: None, - }, - ); - - assert_eq!(actual, Some(8.75)); - } - - #[test] - fn total_cost_should_fallback_to_usage_summary_when_logs_have_no_usage() { - let mut summary = empty_usage_summary(); - summary.input_tokens = 100_000; - summary.output_tokens = 100_000; - - let actual = total_cost( - &[], - &summary, - BillingContext { - default_model: "gpt-5.5", - default_service_tier: None, - }, - ); - - assert_eq!(actual, 3.5); - } - - #[test] - fn dashboard_cards_should_calculate_rpm_tpm_from_today_window() { - let now = Utc::now(); - let logs = vec![ - usage_log_with_tokens(now, 10), - usage_log_with_tokens(now, 20), - usage_log_with_tokens(day_start(now) - chrono::Duration::seconds(1), 30), - ]; - - let actual = dashboard_cards(&[], &empty_usage_summary(), &logs, "gpt-5.5", None); - - assert_eq!((actual.traffic.rpm, actual.traffic.tpm), (2, 30)); - } - - #[test] - fn dashboard_cards_should_report_first_token_and_completion_latency_separately() { - let now = Utc::now(); - let mut first = usage_log_with_tokens(now, 10); - first.latency_ms = Some(1_000); - first.metadata["firstTokenMs"] = json!(200u64); - let mut second = usage_log_with_tokens(now, 20); - second.latency_ms = Some(2_000); - second.metadata["firstTokenMs"] = json!(400u64); - let logs = vec![first, second]; - - let actual = dashboard_cards(&[], &empty_usage_summary(), &logs, "gpt-5.5", None); - - assert_eq!( - ( - actual.cache.first_token_latency_ms, - actual.cache.completion_latency_ms, - ), - (Some(300), Some(1_500)), - ); - } - - fn usage_log_with_tokens(created_at: chrono::DateTime, total_tokens: u64) -> EventLog { - usage_log( - created_at, - "gpt-5.5", - json!({ - "usage": { - "inputTokens": total_tokens, - "outputTokens": 0u64, - "cachedTokens": 0u64 - } - }), - ) - } - - fn usage_log( - created_at: chrono::DateTime, - model: &str, - metadata: serde_json::Value, - ) -> EventLog { - let mut log = EventLog::new("v1.response", EventLevel::Info, "completed"); - log.model = Some(model.to_string()); - log.created_at = created_at; - log.metadata = metadata; - log - } -} diff --git a/src/upstream/accounts/model.rs b/src/upstream/accounts/model.rs index f586305c7..7ea8bf32d 100644 --- a/src/upstream/accounts/model.rs +++ b/src/upstream/accounts/model.rs @@ -1,6 +1,6 @@ //! 账号领域模型。 -use chrono::{DateTime, Duration, Utc}; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; /// 账号当前状态。 @@ -106,48 +106,6 @@ pub struct Account { pub last_used_at: Option, } -impl Account { - /// 构造测试默认账号。 - pub fn test(id: &str, status: AccountStatus) -> Self { - Self { - id: id.to_string(), - email: None, - account_id: None, - user_id: None, - label: None, - plan_type: None, - access_token: format!("token-{id}"), - refresh_token: Some(format!("refresh-{id}")), - access_token_expires_at: Some(Utc::now() + Duration::hours(1)), - next_refresh_at: None, - status, - quota_limit_reached: false, - quota_verify_required: false, - quota_cooldown_until: None, - cloudflare_cooldown_until: None, - request_count: 0, - empty_response_count: 0, - image_input_tokens: 0, - image_output_tokens: 0, - image_request_count: 0, - image_request_failed_count: 0, - window_request_count: 0, - window_input_tokens: 0, - window_output_tokens: 0, - window_cached_tokens: 0, - window_image_input_tokens: 0, - window_image_output_tokens: 0, - window_image_request_count: 0, - window_image_request_failed_count: 0, - window_started_at: None, - window_reset_at: None, - limit_window_seconds: None, - added_at: Utc::now().to_rfc3339(), - last_used_at: None, - } - } -} - /// 账号用量增量(来自 usage.rs)。 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct AccountUsageDelta { diff --git a/src/upstream/fingerprint.rs b/src/upstream/fingerprint.rs index ef7441d3e..6bd5055f3 100644 --- a/src/upstream/fingerprint.rs +++ b/src/upstream/fingerprint.rs @@ -90,11 +90,6 @@ impl Fingerprint { } } - /// 返回测试用默认指纹。 - pub fn default_for_tests() -> Self { - Self::default_codex_desktop() - } - /// 返回默认请求头集合。 pub fn default_headers() -> IndexMap { let mut headers = IndexMap::new(); diff --git a/tests/admin/accounts/import_export.rs b/tests/admin/accounts/import_export.rs index 213ca5c14..048c5b2d5 100644 --- a/tests/admin/accounts/import_export.rs +++ b/tests/admin/accounts/import_export.rs @@ -42,7 +42,7 @@ async fn admin_accounts_export_should_return_native_account_tokens() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -100,7 +100,7 @@ async fn admin_accounts_import_should_store_native_account_tokens() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -301,7 +301,7 @@ async fn admin_accounts_import_should_fetch_usage_to_complete_missing_plan_and_q client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -490,7 +490,7 @@ async fn admin_accounts_import_should_require_admin_session_cookie() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/admin/accounts/lifecycle.rs b/tests/admin/accounts/lifecycle.rs index 124641b88..fa307946d 100644 --- a/tests/admin/accounts/lifecycle.rs +++ b/tests/admin/accounts/lifecycle.rs @@ -23,7 +23,7 @@ async fn admin_accounts_lifecycle_should_update_and_delete_accounts() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -322,7 +322,7 @@ async fn admin_account_cookies_should_store_encrypted_cookie_header() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -406,7 +406,7 @@ async fn admin_account_create_should_derive_claims_and_store_encrypted_tokens() client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -452,7 +452,7 @@ async fn admin_account_manual_create_should_reject_missing_invalid_expired_or_un client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/admin/accounts/list.rs b/tests/admin/accounts/list.rs index 71da72b03..e7b7f55eb 100644 --- a/tests/admin/accounts/list.rs +++ b/tests/admin/accounts/list.rs @@ -24,7 +24,7 @@ async fn admin_accounts_list_should_not_decrypt_account_tokens() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/admin/accounts/mod.rs b/tests/admin/accounts/mod.rs index 900487644..0fa12a72c 100644 --- a/tests/admin/accounts/mod.rs +++ b/tests/admin/accounts/mod.rs @@ -25,7 +25,7 @@ use codex_proxy_rs::{ store::{NewAccount, SqliteAccountStore}, token_refresh::RefreshLeaseStore, }, - upstream::fingerprint::{Fingerprint, FingerprintRepository}, + upstream::fingerprint::FingerprintRepository, }; use secrecy::{ExposeSecret, SecretString}; use serde_json::{json, Value}; @@ -192,7 +192,7 @@ async fn admin_accounts_test_app_with_overrides( client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/admin/accounts/quota.rs b/tests/admin/accounts/quota.rs index 3ab0ebfcb..e35e8848a 100644 --- a/tests/admin/accounts/quota.rs +++ b/tests/admin/accounts/quota.rs @@ -26,7 +26,7 @@ async fn admin_usage_stats_should_return_page_and_summary() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -91,7 +91,7 @@ async fn admin_usage_stats_should_require_admin_session_cookie() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -162,7 +162,7 @@ async fn admin_usage_stats_should_cursor_page_account_usage() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -248,7 +248,7 @@ async fn admin_account_quota_warnings_should_return_threshold_matches_from_cache client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/admin/auth/session.rs b/tests/admin/auth/session.rs index 8938a8e5c..e7390d387 100644 --- a/tests/admin/auth/session.rs +++ b/tests/admin/auth/session.rs @@ -53,7 +53,7 @@ async fn admin_login_should_issue_http_only_session_cookie() { pool.clone(), ), }; - let fingerprint = codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(codex_proxy_rs::runtime::services::Services::new( &config, stores, @@ -137,7 +137,7 @@ async fn admin_login_should_reject_client_api_key_as_password_or_authorization() pool.clone(), ), }; - let fingerprint = codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(codex_proxy_rs::runtime::services::Services::new( &config, stores, diff --git a/tests/admin/auth/session_routes.rs b/tests/admin/auth/session_routes.rs index 1c1992f7a..29d9fb594 100644 --- a/tests/admin/auth/session_routes.rs +++ b/tests/admin/auth/session_routes.rs @@ -25,7 +25,7 @@ use codex_proxy_rs::{ runtime::state::AppState, upstream::accounts::token_refresh::RefreshLeaseStore, upstream::accounts::{cookies::SqliteCookieStore, store::SqliteAccountStore}, - upstream::fingerprint::{Fingerprint, FingerprintRepository}, + upstream::fingerprint::FingerprintRepository, }; use serde_json::Value; use sqlx::SqlitePool; @@ -51,7 +51,7 @@ async fn admin_login_should_issue_http_only_session_cookie() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -118,7 +118,7 @@ async fn admin_login_should_reject_client_api_key_as_password_or_authorization() client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/admin/keys/mod.rs b/tests/admin/keys/mod.rs index 8d0121260..c2c332787 100644 --- a/tests/admin/keys/mod.rs +++ b/tests/admin/keys/mod.rs @@ -20,7 +20,7 @@ use codex_proxy_rs::{ runtime::state::AppState, upstream::accounts::token_refresh::RefreshLeaseStore, upstream::accounts::{cookies::SqliteCookieStore, store::SqliteAccountStore}, - upstream::fingerprint::{Fingerprint, FingerprintRepository}, + upstream::fingerprint::FingerprintRepository, }; use serde_json::{json, Value}; use sqlx::SqlitePool; @@ -50,7 +50,7 @@ async fn admin_client_key_test_app(db_name: &str) -> (axum::Router, tempfile::Te client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/admin/models/routes.rs b/tests/admin/models/routes.rs index 5ad480366..1eeb92930 100644 --- a/tests/admin/models/routes.rs +++ b/tests/admin/models/routes.rs @@ -25,7 +25,7 @@ use codex_proxy_rs::{ store::{NewAccount, SqliteAccountStore}, token_refresh::RefreshLeaseStore, }, - upstream::fingerprint::{Fingerprint, FingerprintRepository}, + upstream::fingerprint::FingerprintRepository, }; use secrecy::SecretString; use serde_json::{json, Value}; @@ -56,7 +56,7 @@ async fn admin_refresh_models_should_require_admin_session() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -149,7 +149,7 @@ async fn admin_refresh_models_should_store_snapshots_for_distinct_active_plans() client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/admin/monitoring/dashboard.rs b/tests/admin/monitoring/dashboard.rs new file mode 100644 index 000000000..3129d3d2b --- /dev/null +++ b/tests/admin/monitoring/dashboard.rs @@ -0,0 +1,356 @@ +use axum::{ + body::{to_bytes, Body}, + http::{Request, StatusCode}, +}; +use chrono::{Duration, Utc}; +use codex_proxy_rs::{ + admin::{ + auth::service::SqliteAdminSessionStore, + keys::service::SqliteClientKeyStore, + monitoring::{ + event_store::SqliteEventLogStore, + events::{EventLevel, EventLog}, + }, + }, + infra::{crypto::SecretBox, database::connect_sqlite, identity::ApiKeyHasher}, + proxy::dispatch::session_affinity::SqliteSessionAffinityStore, + runtime::{ + services::{BackgroundTaskStores, Services}, + state::AppState, + }, + upstream::{ + accounts::{ + cookies::SqliteCookieStore, store::SqliteAccountStore, token_refresh::RefreshLeaseStore, + }, + fingerprint::{Fingerprint, FingerprintRepository}, + }, +}; +use serde_json::{json, Value}; +use sqlx::SqlitePool; +use tower::util::ServiceExt; + +#[tokio::test] +async fn dashboard_summary_should_render_fingerprint_updated_at_as_beijing_time() { + let (app, _store, _pool, _dir) = dashboard_test_app( + "dashboard-fingerprint-updated.sqlite", + crate::support::fingerprint::test_fingerprint_with_updated_at(Some( + "2026-06-23T11:36:46.965574590+00:00", + )), + ) + .await; + + let body = dashboard_summary(app).await; + + assert_eq!( + service_status_value(&body, "更新时间"), + "2026-06-23 19:36:46" + ); +} + +#[tokio::test] +async fn dashboard_summary_should_render_dash_when_fingerprint_updated_at_is_missing() { + let (app, _store, _pool, _dir) = dashboard_test_app( + "dashboard-fingerprint-missing.sqlite", + crate::support::fingerprint::test_fingerprint(), + ) + .await; + + let body = dashboard_summary(app).await; + + assert_eq!(service_status_value(&body, "更新时间"), "-"); +} + +#[tokio::test] +async fn dashboard_summary_should_calculate_today_traffic_and_latency_from_event_logs() { + let (app, store, _pool, _dir) = dashboard_test_app( + "dashboard-traffic-latency.sqlite", + crate::support::fingerprint::test_fingerprint(), + ) + .await; + let now = Utc::now(); + let mut first = usage_log_with_tokens(now, 10); + first.latency_ms = Some(1_000); + first.metadata["firstTokenMs"] = json!(200u64); + let mut second = usage_log_with_tokens(now, 20); + second.latency_ms = Some(2_000); + second.metadata["firstTokenMs"] = json!(400u64); + store.append(&first).await.unwrap(); + store.append(&second).await.unwrap(); + store + .append(&usage_log_with_tokens(now - Duration::days(2), 30)) + .await + .unwrap(); + + let body = dashboard_summary(app).await; + + assert_eq!(body["data"]["cards"]["traffic"]["rpm"], 2); + assert_eq!(body["data"]["cards"]["traffic"]["tpm"], 30); + assert_eq!( + body["data"]["cards"]["cache"]["firstTokenLatencyMs"] + .as_u64() + .unwrap(), + 300 + ); + assert_eq!( + body["data"]["cards"]["cache"]["completionLatencyMs"] + .as_u64() + .unwrap(), + 1_500 + ); +} + +#[tokio::test] +async fn dashboard_summary_should_calculate_priority_cost_from_event_service_tier() { + let cost = dashboard_summary_total_cost_for_log( + "dashboard-cost-priority.sqlite", + "gpt-5.5", + json!({ + "usage": { + "inputTokens": 100_000u64, + "outputTokens": 100_000u64, + "cachedTokens": 0u64 + }, + "serviceTier": "priority" + }), + ) + .await; + + assert_f64_eq(cost, 8.75); +} + +#[tokio::test] +async fn dashboard_summary_should_charge_cached_input_at_cache_read_price() { + let cost = dashboard_summary_total_cost_for_log( + "dashboard-cost-cached.sqlite", + "gpt-5.5", + json!({ + "usage": { + "inputTokens": 1_000u64, + "outputTokens": 1_000u64, + "cachedTokens": 500u64 + } + }), + ) + .await; + + assert_f64_eq(cost, 0.03275); +} + +#[tokio::test] +async fn dashboard_summary_should_apply_long_context_prices() { + let cost = dashboard_summary_total_cost_for_log( + "dashboard-cost-long-context.sqlite", + "gpt-5.5", + json!({ + "usage": { + "inputTokens": 300_000u64, + "outputTokens": 1_000u64, + "cachedTokens": 0u64 + } + }), + ) + .await; + + assert_f64_eq(cost, 3.045); +} + +#[tokio::test] +async fn dashboard_summary_should_normalize_codex_model_names_for_cost() { + let cost = dashboard_summary_total_cost_for_log( + "dashboard-cost-codex-model.sqlite", + "codex-auto-review", + json!({ + "usage": { + "inputTokens": 100_000u64, + "outputTokens": 100_000u64, + "cachedTokens": 0u64 + } + }), + ) + .await; + + assert_f64_eq(cost, 1.75); +} + +#[tokio::test] +async fn dashboard_summary_should_fallback_total_cost_to_usage_summary_when_logs_have_no_usage() { + let (app, _store, pool, _dir) = dashboard_test_app( + "dashboard-cost-summary-fallback.sqlite", + crate::support::fingerprint::test_fingerprint(), + ) + .await; + seed_usage_summary(&pool, 100_000, 100_000).await; + + let body = dashboard_summary(app).await; + + assert_f64_eq( + body["data"]["cards"]["tokens"]["totalCostUsd"] + .as_f64() + .unwrap(), + 3.5, + ); +} + +async fn dashboard_summary_total_cost_for_log(db_name: &str, model: &str, metadata: Value) -> f64 { + let (app, store, _pool, _dir) = + dashboard_test_app(db_name, crate::support::fingerprint::test_fingerprint()).await; + store + .append(&usage_log(Utc::now(), model, metadata)) + .await + .unwrap(); + + let body = dashboard_summary(app).await; + body["data"]["cards"]["tokens"]["totalCostUsd"] + .as_f64() + .unwrap() +} + +async fn dashboard_test_app( + db_name: &str, + fingerprint: Fingerprint, +) -> ( + axum::Router, + SqliteEventLogStore, + SqlitePool, + tempfile::TempDir, +) { + let dir = tempfile::tempdir().unwrap(); + let db = dir.path().join(db_name); + let url = format!("sqlite://{}", db.display()); + let pool = connect_sqlite(&url).await.unwrap(); + seed_admin_session(&pool, "session_1").await; + let config = crate::support::config::test_config(url); + let secret_box = SecretBox::new([73u8; 32]); + let hasher = ApiKeyHasher::new([74u8; 32]); + let stores = BackgroundTaskStores { + accounts: SqliteAccountStore::new(pool.clone(), secret_box.clone()), + admin_sessions: SqliteAdminSessionStore::new(pool.clone()), + cookies: SqliteCookieStore::new(pool.clone(), secret_box), + fingerprints: FingerprintRepository::new(pool.clone()), + session_affinity: SqliteSessionAffinityStore::new(pool.clone()), + refresh_leases: RefreshLeaseStore::new(pool.clone()), + client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), + event_logs: SqliteEventLogStore::new(pool.clone()), + }; + let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); + let state = AppState { + config, + services: (*services).clone(), + }; + ( + codex_proxy_rs::http::router::router().with_state(state), + SqliteEventLogStore::new(pool.clone()), + pool, + dir, + ) +} + +async fn dashboard_summary(app: axum::Router) -> Value { + let response = app + .oneshot( + Request::builder() + .method("GET") + .uri("/api/admin/dashboard/summary") + .header("cookie", "cpr_admin_session=session_1") + .header("x-request-id", "req_dashboard_summary") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + assert_eq!(response.status(), StatusCode::OK); + response_json(response).await +} + +async fn seed_admin_session(pool: &SqlitePool, session_id: &str) { + sqlx::query( + "insert into admin_users (id, password_hash, created_at, updated_at) values (?, ?, ?, ?)", + ) + .bind("admin_1") + .bind("hash") + .bind("2026-06-18T00:00:00Z") + .bind("2026-06-18T00:00:00Z") + .execute(pool) + .await + .unwrap(); + sqlx::query( + "insert into admin_sessions (id, user_id, expires_at, created_at) values (?, ?, ?, ?)", + ) + .bind(session_id) + .bind("admin_1") + .bind("2999-01-01T00:00:00Z") + .bind("2026-06-18T00:00:00Z") + .execute(pool) + .await + .unwrap(); +} + +async fn seed_usage_summary(pool: &SqlitePool, input_tokens: u64, output_tokens: u64) { + sqlx::query( + "insert into accounts (id, access_token_cipher, status, added_at, updated_at) values (?, ?, ?, ?, ?)", + ) + .bind("acct_usage") + .bind("cipher") + .bind("active") + .bind("2026-06-18T00:00:00Z") + .bind("2026-06-18T00:00:00Z") + .execute(pool) + .await + .unwrap(); + sqlx::query( + "insert into account_usage (account_id, request_count, input_tokens, output_tokens, total_tokens, last_used_at) values (?, ?, ?, ?, ?, ?)", + ) + .bind("acct_usage") + .bind(1_i64) + .bind(input_tokens as i64) + .bind(output_tokens as i64) + .bind((input_tokens + output_tokens) as i64) + .bind("2026-06-18T00:00:00Z") + .execute(pool) + .await + .unwrap(); +} + +async fn response_json(response: axum::response::Response) -> Value { + let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap(); + serde_json::from_slice(&bytes).unwrap() +} + +fn service_status_value<'a>(body: &'a Value, label: &str) -> &'a str { + body["data"]["serviceStatuses"] + .as_array() + .unwrap() + .iter() + .find(|status| status["label"] == label) + .and_then(|status| status["value"].as_str()) + .unwrap() +} + +fn usage_log_with_tokens(created_at: chrono::DateTime, total_tokens: u64) -> EventLog { + usage_log( + created_at, + "gpt-5.5", + json!({ + "usage": { + "inputTokens": total_tokens, + "outputTokens": 0u64, + "cachedTokens": 0u64 + } + }), + ) +} + +fn usage_log(created_at: chrono::DateTime, model: &str, metadata: Value) -> EventLog { + let mut log = EventLog::new("v1.response", EventLevel::Info, "completed"); + log.model = Some(model.to_string()); + log.created_at = created_at; + log.metadata = metadata; + log +} + +fn assert_f64_eq(actual: f64, expected: f64) { + assert!( + (actual - expected).abs() < 1e-12, + "expected {expected}, got {actual}" + ); +} diff --git a/tests/admin/monitoring/logs_routes.rs b/tests/admin/monitoring/logs_routes.rs index 86b6a14a2..757a46972 100644 --- a/tests/admin/monitoring/logs_routes.rs +++ b/tests/admin/monitoring/logs_routes.rs @@ -24,7 +24,7 @@ use codex_proxy_rs::{ runtime::state::AppState, upstream::accounts::token_refresh::RefreshLeaseStore, upstream::accounts::{cookies::SqliteCookieStore, store::SqliteAccountStore}, - upstream::fingerprint::{Fingerprint, FingerprintRepository}, + upstream::fingerprint::FingerprintRepository, }; use serde_json::Value; use sqlx::SqlitePool; @@ -270,7 +270,7 @@ async fn admin_logs_test_app( client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/admin/monitoring/mod.rs b/tests/admin/monitoring/mod.rs index 356e61e5e..f0f63d204 100644 --- a/tests/admin/monitoring/mod.rs +++ b/tests/admin/monitoring/mod.rs @@ -1,2 +1,3 @@ +pub mod dashboard; pub mod events_store; pub mod logs_routes; diff --git a/tests/admin/settings/routes.rs b/tests/admin/settings/routes.rs index 499cbe236..b5cdafb0e 100644 --- a/tests/admin/settings/routes.rs +++ b/tests/admin/settings/routes.rs @@ -20,7 +20,7 @@ use codex_proxy_rs::{ runtime::state::AppState, upstream::accounts::token_refresh::RefreshLeaseStore, upstream::accounts::{cookies::SqliteCookieStore, store::SqliteAccountStore}, - upstream::fingerprint::{Fingerprint, FingerprintRepository}, + upstream::fingerprint::FingerprintRepository, }; use serde_json::{json, Value}; use sqlx::SqlitePool; @@ -102,7 +102,7 @@ async fn admin_settings_update_should_persist_retained_fields_to_config_yaml() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let mut services = Services::new(&config, stores, fingerprint); services.settings = std::sync::Arc::new( codex_proxy_rs::config::settings::RuntimeSettingsService::with_config_path( @@ -189,7 +189,7 @@ async fn admin_settings_update_should_reject_unsupported_or_invalid_fields() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -234,7 +234,7 @@ async fn admin_settings_test_app(db_name: &str) -> (axum::Router, tempfile::Temp client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/http/web_assets/mod.rs b/tests/http/web_assets/mod.rs index e5d25d61c..665c49d6b 100644 --- a/tests/http/web_assets/mod.rs +++ b/tests/http/web_assets/mod.rs @@ -73,7 +73,7 @@ async fn server_router_should_serve_frontend_assets_without_shadowing_api_routes pool.clone(), ), }; - let fingerprint = codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(codex_proxy_rs::runtime::services::Services::new( &config, stores, diff --git a/tests/proxy/dispatch/chat_upstream/mod.rs b/tests/proxy/dispatch/chat_upstream/mod.rs index 1ece0f544..e6de2fff4 100644 --- a/tests/proxy/dispatch/chat_upstream/mod.rs +++ b/tests/proxy/dispatch/chat_upstream/mod.rs @@ -35,7 +35,7 @@ use codex_proxy_rs::{ upstream::accounts::{ cookies::SqliteCookieStore, store::SqliteAccountStore, token_refresh::RefreshLeaseStore, }, - upstream::fingerprint::{Fingerprint, FingerprintRepository}, + upstream::fingerprint::FingerprintRepository, }; use futures::{SinkExt, StreamExt}; use secrecy::SecretString; @@ -257,7 +257,7 @@ fn test_app_state_with_pool_secret_api_key_hasher_and_installation_id( let services = Services::with_installation_id( &config, stores, - Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), Some(installation_id), ); AppState { config, services } diff --git a/tests/proxy/openai/models_routes.rs b/tests/proxy/openai/models_routes.rs index f2a4b763f..b042ba46e 100644 --- a/tests/proxy/openai/models_routes.rs +++ b/tests/proxy/openai/models_routes.rs @@ -20,7 +20,7 @@ use codex_proxy_rs::{ runtime::state::AppState, upstream::accounts::token_refresh::RefreshLeaseStore, upstream::accounts::{cookies::SqliteCookieStore, store::SqliteAccountStore}, - upstream::fingerprint::{Fingerprint, FingerprintRepository}, + upstream::fingerprint::FingerprintRepository, }; use sqlx::SqlitePool; use tower::util::ServiceExt; @@ -44,7 +44,7 @@ async fn models_route_should_reject_unknown_client_api_key() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher.clone()), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, @@ -86,7 +86,7 @@ async fn models_route_should_accept_stored_client_api_key() { client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/proxy/openai/responses_routes.rs b/tests/proxy/openai/responses_routes.rs index 069f6d970..24630e9a9 100644 --- a/tests/proxy/openai/responses_routes.rs +++ b/tests/proxy/openai/responses_routes.rs @@ -20,7 +20,7 @@ use codex_proxy_rs::{ runtime::state::AppState, upstream::accounts::token_refresh::RefreshLeaseStore, upstream::accounts::{cookies::SqliteCookieStore, store::SqliteAccountStore}, - upstream::fingerprint::{Fingerprint, FingerprintRepository}, + upstream::fingerprint::FingerprintRepository, }; use serde_json::Value; use sqlx::SqlitePool; @@ -153,7 +153,7 @@ async fn test_app_with_client_api_key() -> (axum::Router, String, tempfile::Temp client_keys: SqliteClientKeyStore::new(pool.clone(), hasher), event_logs: SqliteEventLogStore::new(pool.clone()), }; - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint)); let state = AppState { config, diff --git a/tests/runtime/services.rs b/tests/runtime/services.rs index acf1981bf..acc9c5ef6 100644 --- a/tests/runtime/services.rs +++ b/tests/runtime/services.rs @@ -11,7 +11,7 @@ use codex_proxy_rs::{ accounts::{ cookies::SqliteCookieStore, store::SqliteAccountStore, token_refresh::RefreshLeaseStore, }, - fingerprint::{Fingerprint, FingerprintRepository}, + fingerprint::FingerprintRepository, transport::CODEX_CA_CERT_ENV, }, }; @@ -45,7 +45,11 @@ async fn services_try_new_should_use_configured_tls_transport_builder() { event_logs: SqliteEventLogStore::new(pool), }; - let error = match Services::try_new(&config, stores, Fingerprint::default_for_tests()) { + let error = match Services::try_new( + &config, + stores, + crate::support::fingerprint::test_fingerprint(), + ) { Ok(_) => panic!("invalid custom CA should fail service transport construction"), Err(error) => error, }; diff --git a/tests/runtime/tasks/fingerprint.rs b/tests/runtime/tasks/fingerprint.rs index 25b4c0e42..178bf1421 100644 --- a/tests/runtime/tasks/fingerprint.rs +++ b/tests/runtime/tasks/fingerprint.rs @@ -8,11 +8,9 @@ async fn fingerprint_update_task_should_start_background_checker() { .await .expect("sqlite pool"); let repo = FingerprintRepository::new(pool); - repo.ensure_current_seed( - &codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), - ) - .await - .expect("seed current fingerprint"); + repo.ensure_current_seed(&crate::support::fingerprint::test_fingerprint()) + .await + .expect("seed current fingerprint"); let handle = codex_proxy_rs::runtime::tasks::fingerprint_update::start_fingerprint_update_task( Some(repo), @@ -48,11 +46,9 @@ async fn fingerprint_update_task_should_apply_initial_appcast_update_to_reposito .await .expect("sqlite pool"); let repo = FingerprintRepository::new(pool); - repo.ensure_current_seed( - &codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), - ) - .await - .expect("seed current fingerprint"); + repo.ensure_current_seed(&crate::support::fingerprint::test_fingerprint()) + .await + .expect("seed current fingerprint"); let extracted_path = dir.path().join("extracted-fingerprint.json"); std::fs::write( &extracted_path, @@ -104,11 +100,9 @@ async fn fingerprint_update_task_should_not_persist_when_appcast_matches_current .await .expect("sqlite pool"); let repo = FingerprintRepository::new(pool); - repo.ensure_current_seed( - &codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), - ) - .await - .expect("seed current fingerprint"); + repo.ensure_current_seed(&crate::support::fingerprint::test_fingerprint()) + .await + .expect("seed current fingerprint"); let handle = codex_proxy_rs::runtime::tasks::fingerprint_update::start_fingerprint_update_task( Some(repo.clone()), format!("{}/appcast.xml", server.uri()), diff --git a/tests/runtime/tasks/mod.rs b/tests/runtime/tasks/mod.rs index 00adc4e99..aec76af9f 100644 --- a/tests/runtime/tasks/mod.rs +++ b/tests/runtime/tasks/mod.rs @@ -29,7 +29,10 @@ struct FakeAccountStore; #[async_trait] impl AccountStore for FakeAccountStore { async fn list_pool_accounts(&self) -> AccountStoreResult> { - Ok(vec![Account::test("acct-1", AccountStatus::Active)]) + Ok(vec![crate::support::accounts::test_account( + "acct-1", + AccountStatus::Active, + )]) } async fn mark_quota_limited_until( diff --git a/tests/support/accounts.rs b/tests/support/accounts.rs new file mode 100644 index 000000000..b7cf0e5d8 --- /dev/null +++ b/tests/support/accounts.rs @@ -0,0 +1,41 @@ +use chrono::{Duration, Utc}; +use codex_proxy_rs::upstream::accounts::model::{Account, AccountStatus}; + +pub fn test_account(id: &str, status: AccountStatus) -> Account { + Account { + id: id.to_string(), + email: None, + account_id: None, + user_id: None, + label: None, + plan_type: None, + access_token: format!("token-{id}"), + refresh_token: Some(format!("refresh-{id}")), + access_token_expires_at: Some(Utc::now() + Duration::hours(1)), + next_refresh_at: None, + status, + quota_limit_reached: false, + quota_verify_required: false, + quota_cooldown_until: None, + cloudflare_cooldown_until: None, + request_count: 0, + empty_response_count: 0, + image_input_tokens: 0, + image_output_tokens: 0, + image_request_count: 0, + image_request_failed_count: 0, + window_request_count: 0, + window_input_tokens: 0, + window_output_tokens: 0, + window_cached_tokens: 0, + window_image_input_tokens: 0, + window_image_output_tokens: 0, + window_image_request_count: 0, + window_image_request_failed_count: 0, + window_started_at: None, + window_reset_at: None, + limit_window_seconds: None, + added_at: Utc::now().to_rfc3339(), + last_used_at: None, + } +} diff --git a/tests/support/fingerprint.rs b/tests/support/fingerprint.rs new file mode 100644 index 000000000..93784788e --- /dev/null +++ b/tests/support/fingerprint.rs @@ -0,0 +1,11 @@ +use codex_proxy_rs::upstream::fingerprint::Fingerprint; + +pub fn test_fingerprint() -> Fingerprint { + Fingerprint::default_codex_desktop() +} + +pub fn test_fingerprint_with_updated_at(updated_at: Option<&str>) -> Fingerprint { + let mut fingerprint = test_fingerprint(); + fingerprint.updated_at = updated_at.map(ToString::to_string); + fingerprint +} diff --git a/tests/support/mod.rs b/tests/support/mod.rs index 40504df15..276c62dda 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -1,2 +1,4 @@ +pub mod accounts; pub mod config; +pub mod fingerprint; pub mod sqlite; diff --git a/tests/upstream/accounts/account_pool/mod.rs b/tests/upstream/accounts/account_pool/mod.rs index 955162f57..b8b72b723 100644 --- a/tests/upstream/accounts/account_pool/mod.rs +++ b/tests/upstream/accounts/account_pool/mod.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _}; use chrono::{Duration, TimeZone, Utc}; use codex_proxy_rs::upstream::accounts::{ - model::{Account, AccountStatus}, + model::AccountStatus, pool::{ AccountAcquireRequest, AccountPool, AccountPoolOptions, AccountWindowUsageDelta, RotationStrategy, diff --git a/tests/upstream/accounts/account_pool/quota.rs b/tests/upstream/accounts/account_pool/quota.rs index 56a32931a..2b353cc1b 100644 --- a/tests/upstream/accounts/account_pool/quota.rs +++ b/tests/upstream/accounts/account_pool/quota.rs @@ -2,11 +2,14 @@ use super::*; #[test] fn account_pool_should_skip_accounts_with_cached_quota_limit() { - let mut limited = Account::test("limited", AccountStatus::Active); + let mut limited = crate::support::accounts::test_account("limited", AccountStatus::Active); limited.quota_limit_reached = true; let mut pool = AccountPool::default(); pool.insert(limited); - pool.insert(Account::test("usable", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "usable", + AccountStatus::Active, + )); assert_eq!(pool.acquire("gpt-5.5").unwrap().id, "usable"); } @@ -15,7 +18,10 @@ fn account_pool_should_skip_accounts_with_cached_quota_limit() { fn account_pool_should_reuse_quota_limited_accounts_after_cooldown() { let now = fixed_time(); let mut pool = AccountPool::default(); - pool.insert(Account::test("limited", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "limited", + AccountStatus::Active, + )); pool.mark_quota_limited_until("limited", now + Duration::seconds(30)); assert!(pool @@ -37,7 +43,10 @@ fn account_pool_should_reuse_quota_limited_accounts_after_cooldown() { fn account_pool_should_not_shorten_existing_quota_cooldown() { let now = fixed_time(); let mut pool = AccountPool::default(); - pool.insert(Account::test("limited", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "limited", + AccountStatus::Active, + )); pool.mark_quota_limited_until("limited", now + Duration::seconds(180)); pool.mark_quota_limited_until("limited", now + Duration::seconds(60)); @@ -53,7 +62,7 @@ fn account_pool_should_not_shorten_existing_quota_cooldown() { #[test] fn account_pool_should_not_replace_known_window_length_with_cooldown_seconds() { let now = Utc::now(); - let mut account = Account::test("limited", AccountStatus::Active); + let mut account = crate::support::accounts::test_account("limited", AccountStatus::Active); account.limit_window_seconds = Some(300); let mut pool = AccountPool::default(); pool.insert(account); @@ -72,7 +81,7 @@ fn account_pool_should_not_replace_known_window_length_with_cooldown_seconds() { #[test] fn acquire_should_refresh_expired_cooldowns_before_selecting_account() { let now = fixed_time(); - let mut account = Account::test("acct_a", AccountStatus::Active); + let mut account = crate::support::accounts::test_account("acct_a", AccountStatus::Active); account.quota_limit_reached = true; account.quota_cooldown_until = Some(now - Duration::seconds(1)); account.cloudflare_cooldown_until = Some(now - Duration::seconds(1)); @@ -100,11 +109,14 @@ fn acquire_should_refresh_expired_cooldowns_before_selecting_account() { #[test] fn account_pool_should_skip_accounts_in_cloudflare_cooldown() { let now = fixed_time(); - let mut cooling = Account::test("cooling", AccountStatus::Active); + let mut cooling = crate::support::accounts::test_account("cooling", AccountStatus::Active); cooling.cloudflare_cooldown_until = Some(now + Duration::seconds(30)); let mut pool = AccountPool::default(); pool.insert(cooling); - pool.insert(Account::test("usable", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "usable", + AccountStatus::Active, + )); let acquired = pool .acquire_with(AccountAcquireRequest::new("gpt-5.5", now)) @@ -120,9 +132,9 @@ fn least_used_should_deprioritize_quota_limited_accounts_when_skip_is_disabled() rotation_strategy: RotationStrategy::LeastUsed, ..AccountPoolOptions::default() }); - let mut limited = Account::test("limited", AccountStatus::Active); + let mut limited = crate::support::accounts::test_account("limited", AccountStatus::Active); limited.quota_limit_reached = true; - let mut usable = Account::test("usable", AccountStatus::Active); + let mut usable = crate::support::accounts::test_account("usable", AccountStatus::Active); usable.request_count = 100; pool.insert(limited); pool.insert(usable); @@ -139,9 +151,9 @@ fn least_used_should_prefer_earlier_rate_limit_window_reset() { rotation_strategy: RotationStrategy::LeastUsed, ..AccountPoolOptions::default() }); - let mut soon = Account::test("soon", AccountStatus::Active); + let mut soon = crate::support::accounts::test_account("soon", AccountStatus::Active); soon.window_reset_at = Some(now + Duration::seconds(30)); - let mut later = Account::test("later", AccountStatus::Active); + let mut later = crate::support::accounts::test_account("later", AccountStatus::Active); later.window_reset_at = Some(now + Duration::seconds(300)); pool.insert(later); pool.insert(soon); @@ -156,7 +168,7 @@ fn least_used_should_prefer_earlier_rate_limit_window_reset() { #[test] fn sync_rate_limit_window_should_reset_window_counters_when_drift_crosses_threshold() { let now = fixed_time(); - let mut account = Account::test("acct_a", AccountStatus::Active); + let mut account = crate::support::accounts::test_account("acct_a", AccountStatus::Active); account.window_request_count = 5; account.window_input_tokens = 100; account.window_output_tokens = 40; diff --git a/tests/upstream/accounts/account_pool/selection.rs b/tests/upstream/accounts/account_pool/selection.rs index 5aff9701e..e2e266ff3 100644 --- a/tests/upstream/accounts/account_pool/selection.rs +++ b/tests/upstream/accounts/account_pool/selection.rs @@ -6,8 +6,14 @@ fn account_pool_should_respect_max_concurrent_slots_per_account() { max_concurrent_per_account: 1, ..AccountPoolOptions::default() }); - pool.insert(Account::test("acct_a", AccountStatus::Active)); - pool.insert(Account::test("acct_b", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "acct_a", + AccountStatus::Active, + )); + pool.insert(crate::support::accounts::test_account( + "acct_b", + AccountStatus::Active, + )); let first = pool.acquire("gpt-5.5").unwrap(); let second = pool.acquire("gpt-5.5").unwrap(); @@ -27,9 +33,18 @@ fn account_pool_should_rotate_round_robin_across_candidates() { rotation_strategy: RotationStrategy::RoundRobin, ..AccountPoolOptions::default() }); - pool.insert(Account::test("acct_a", AccountStatus::Active)); - pool.insert(Account::test("acct_b", AccountStatus::Active)); - pool.insert(Account::test("acct_c", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "acct_a", + AccountStatus::Active, + )); + pool.insert(crate::support::accounts::test_account( + "acct_b", + AccountStatus::Active, + )); + pool.insert(crate::support::accounts::test_account( + "acct_c", + AccountStatus::Active, + )); assert_eq!(pool.acquire("gpt-5.5").unwrap().id, "acct_a"); assert_eq!(pool.acquire("gpt-5.5").unwrap().id, "acct_c"); @@ -40,7 +55,7 @@ fn account_pool_should_rotate_round_robin_across_candidates() { #[test] fn acquire_should_skip_accounts_with_expired_token_metadata() { let now = fixed_time(); - let mut expired = Account::test("expired", AccountStatus::Active); + let mut expired = crate::support::accounts::test_account("expired", AccountStatus::Active); expired.access_token_expires_at = Some(now - Duration::seconds(1)); let mut pool = AccountPool::default(); pool.insert(expired); @@ -53,7 +68,7 @@ fn acquire_should_skip_accounts_with_expired_token_metadata() { #[test] fn acquire_should_skip_accounts_with_expired_jwt_when_metadata_is_missing() { let now = Utc::now(); - let mut expired = Account::test("expired", AccountStatus::Active); + let mut expired = crate::support::accounts::test_account("expired", AccountStatus::Active); expired.access_token = test_jwt(-60); expired.access_token_expires_at = None; let mut pool = AccountPool::default(); @@ -114,9 +129,9 @@ fn account_pool_should_prefer_configured_tier_priority() { tier_priority: vec!["team".to_string(), "plus".to_string()], ..AccountPoolOptions::default() }); - let mut free = Account::test("free", AccountStatus::Active); + let mut free = crate::support::accounts::test_account("free", AccountStatus::Active); free.plan_type = Some("free".to_string()); - let mut team = Account::test("team", AccountStatus::Active); + let mut team = crate::support::accounts::test_account("team", AccountStatus::Active); team.plan_type = Some("team".to_string()); pool.insert(free); pool.insert(team); @@ -132,9 +147,9 @@ fn account_pool_should_filter_by_model_plan_allowlist() { model_plan_allowlist: model_plans, ..AccountPoolOptions::default() }); - let mut free = Account::test("free", AccountStatus::Active); + let mut free = crate::support::accounts::test_account("free", AccountStatus::Active); free.plan_type = Some("free".to_string()); - let mut plus = Account::test("plus", AccountStatus::Active); + let mut plus = crate::support::accounts::test_account("plus", AccountStatus::Active); plus.plan_type = Some("plus".to_string()); pool.insert(free); pool.insert(plus); @@ -145,8 +160,14 @@ fn account_pool_should_filter_by_model_plan_allowlist() { #[test] fn account_pool_should_exclude_requested_account_ids() { let mut pool = AccountPool::default(); - pool.insert(Account::test("acct_a", AccountStatus::Active)); - pool.insert(Account::test("acct_b", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "acct_a", + AccountStatus::Active, + )); + pool.insert(crate::support::accounts::test_account( + "acct_b", + AccountStatus::Active, + )); let acquired = pool .acquire_with( @@ -161,8 +182,14 @@ fn account_pool_should_exclude_requested_account_ids() { #[test] fn account_pool_should_prefer_session_affinity_account_when_available() { let mut pool = AccountPool::default(); - pool.insert(Account::test("acct_a", AccountStatus::Active)); - pool.insert(Account::test("acct_b", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "acct_a", + AccountStatus::Active, + )); + pool.insert(crate::support::accounts::test_account( + "acct_b", + AccountStatus::Active, + )); let acquired = pool .acquire_with( @@ -181,7 +208,10 @@ fn account_pool_should_cleanup_stale_slots_before_acquire() { stale_slot_ttl: Duration::minutes(5), ..AccountPoolOptions::default() }); - pool.insert(Account::test("acct_a", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "acct_a", + AccountStatus::Active, + )); assert!(pool .acquire_with(AccountAcquireRequest::new("gpt-5.5", now)) @@ -203,8 +233,14 @@ fn account_pool_should_rotate_tied_least_used_accounts() { rotation_strategy: RotationStrategy::LeastUsed, ..AccountPoolOptions::default() }); - pool.insert(Account::test("acct_a", AccountStatus::Active)); - pool.insert(Account::test("acct_b", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "acct_a", + AccountStatus::Active, + )); + pool.insert(crate::support::accounts::test_account( + "acct_b", + AccountStatus::Active, + )); let first = pool.acquire("gpt-5.5").unwrap(); pool.release(&first.id); @@ -220,9 +256,9 @@ fn least_used_should_prefer_lower_runtime_request_count() { rotation_strategy: RotationStrategy::LeastUsed, ..AccountPoolOptions::default() }); - let mut busy = Account::test("busy", AccountStatus::Active); + let mut busy = crate::support::accounts::test_account("busy", AccountStatus::Active); busy.request_count = 10; - let mut quiet = Account::test("quiet", AccountStatus::Active); + let mut quiet = crate::support::accounts::test_account("quiet", AccountStatus::Active); quiet.request_count = 2; pool.insert(busy); pool.insert(quiet); diff --git a/tests/upstream/accounts/account_pool/usage_window.rs b/tests/upstream/accounts/account_pool/usage_window.rs index b8fd75c37..614a13202 100644 --- a/tests/upstream/accounts/account_pool/usage_window.rs +++ b/tests/upstream/accounts/account_pool/usage_window.rs @@ -3,7 +3,7 @@ use super::*; #[test] fn reset_usage_should_clear_runtime_counters_and_preserve_window_reset() { let now = fixed_time(); - let mut account = Account::test("acct_a", AccountStatus::Active); + let mut account = crate::support::accounts::test_account("acct_a", AccountStatus::Active); account.request_count = 7; account.empty_response_count = 2; account.image_input_tokens = 11; @@ -59,7 +59,10 @@ fn account_pool_should_return_previous_slot_time_for_request_staggering() { max_concurrent_per_account: 2, ..AccountPoolOptions::default() }); - pool.insert(Account::test("acct_a", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "acct_a", + AccountStatus::Active, + )); let first = pool .acquire_with(AccountAcquireRequest::new("gpt-5.5", now)) @@ -82,8 +85,14 @@ fn account_pool_should_report_capacity_summary() { max_concurrent_per_account: 2, ..AccountPoolOptions::default() }); - pool.insert(Account::test("acct_a", AccountStatus::Active)); - pool.insert(Account::test("acct_b", AccountStatus::Disabled)); + pool.insert(crate::support::accounts::test_account( + "acct_a", + AccountStatus::Active, + )); + pool.insert(crate::support::accounts::test_account( + "acct_b", + AccountStatus::Disabled, + )); pool.acquire_with(AccountAcquireRequest::new("gpt-5.5", now)); let summary = pool.capacity_summary(now); @@ -100,10 +109,12 @@ fn least_used_should_compare_request_count_when_only_one_account_has_window_rese rotation_strategy: RotationStrategy::LeastUsed, ..AccountPoolOptions::default() }); - let mut reset_known = Account::test("reset_known", AccountStatus::Active); + let mut reset_known = + crate::support::accounts::test_account("reset_known", AccountStatus::Active); reset_known.window_reset_at = Some(now + Duration::seconds(30)); reset_known.request_count = 10; - let mut reset_unknown = Account::test("reset_unknown", AccountStatus::Active); + let mut reset_unknown = + crate::support::accounts::test_account("reset_unknown", AccountStatus::Active); reset_unknown.request_count = 2; pool.insert(reset_known); pool.insert(reset_unknown); @@ -119,7 +130,7 @@ fn least_used_should_compare_request_count_when_only_one_account_has_window_rese fn acquire_should_mark_runtime_usage_window() { let now = fixed_time(); let mut pool = AccountPool::default(); - let mut account = Account::test("acct_a", AccountStatus::Active); + let mut account = crate::support::accounts::test_account("acct_a", AccountStatus::Active); account.limit_window_seconds = Some(60); pool.insert(account); @@ -144,7 +155,10 @@ fn acquire_should_mark_runtime_usage_window() { fn record_window_token_usage_should_accumulate_runtime_window_tokens() { let now = fixed_time(); let mut pool = AccountPool::default(); - pool.insert(Account::test("acct_a", AccountStatus::Active)); + pool.insert(crate::support::accounts::test_account( + "acct_a", + AccountStatus::Active, + )); pool.record_window_token_usage( "acct_a", diff --git a/tests/upstream/accounts/quota_refresh.rs b/tests/upstream/accounts/quota_refresh.rs index f366c1b23..89c7ec6d3 100644 --- a/tests/upstream/accounts/quota_refresh.rs +++ b/tests/upstream/accounts/quota_refresh.rs @@ -6,7 +6,6 @@ use codex_proxy_rs::infra::database::connect_sqlite; use codex_proxy_rs::upstream::accounts::model::AccountStatus; use codex_proxy_rs::upstream::accounts::quota::RuntimeQuotaRefreshService; use codex_proxy_rs::upstream::accounts::store::{NewAccount, SqliteAccountStore}; -use codex_proxy_rs::upstream::fingerprint::Fingerprint; use codex_proxy_rs::upstream::transport::CodexBackendClient; use secrecy::SecretString; use serde_json::{json, Value}; @@ -78,7 +77,7 @@ async fn quota_refresh_service_should_fetch_usage_for_quota_locked_accounts_and_ let codex = CodexBackendClient::new( reqwest::Client::new(), server.uri(), - Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); let service = RuntimeQuotaRefreshService::new(store.clone(), Arc::new(codex)); @@ -141,7 +140,7 @@ async fn quota_refresh_service_should_stagger_multiple_locked_account_requests() let codex = CodexBackendClient::new( reqwest::Client::new(), server.uri(), - Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); let service = RuntimeQuotaRefreshService::new(store, Arc::new(codex)) .with_request_spacing(StdDuration::from_millis(200)); diff --git a/tests/upstream/fingerprint/mod.rs b/tests/upstream/fingerprint/mod.rs index 2d1f96c80..2dc7c90f6 100644 --- a/tests/upstream/fingerprint/mod.rs +++ b/tests/upstream/fingerprint/mod.rs @@ -1,6 +1,6 @@ use codex_proxy_rs::infra::database::connect_sqlite; use codex_proxy_rs::upstream::fingerprint::{ - parse_update_manifest, Fingerprint, FingerprintRepository, FingerprintUpdater, UpdateChecker, + parse_update_manifest, FingerprintRepository, FingerprintUpdater, UpdateChecker, }; #[test] @@ -20,7 +20,7 @@ async fn fingerprint_repository_should_update_current_record() { .await .expect("sqlite pool"); let repo = FingerprintRepository::new(pool.clone()); - let default_fingerprint = Fingerprint::default_for_tests(); + let default_fingerprint = crate::support::fingerprint::test_fingerprint(); repo.ensure_current_seed(&default_fingerprint) .await @@ -66,7 +66,7 @@ async fn fingerprint_updater_should_fetch_manifest_and_persist_history() { .await .expect("sqlite pool"); let repo = FingerprintRepository::new(pool); - repo.ensure_current_seed(&Fingerprint::default_for_tests()) + repo.ensure_current_seed(&crate::support::fingerprint::test_fingerprint()) .await .expect("seed current fingerprint"); let updater = FingerprintUpdater::new( @@ -151,7 +151,7 @@ async fn update_checker_should_apply_available_update_to_repository() { .await .expect("sqlite pool"); let repo = FingerprintRepository::new(pool); - repo.ensure_current_seed(&Fingerprint::default_for_tests()) + repo.ensure_current_seed(&crate::support::fingerprint::test_fingerprint()) .await .expect("seed current fingerprint"); let extracted_path = dir.path().join("extracted-fingerprint.json"); diff --git a/tests/upstream/models/catalog.rs b/tests/upstream/models/catalog.rs index b036ae531..816d59284 100644 --- a/tests/upstream/models/catalog.rs +++ b/tests/upstream/models/catalog.rs @@ -227,7 +227,7 @@ fn backend_models(model_id: &str) -> Vec { } fn active_account(id: &str, plan_type: &str) -> Account { - let mut account = Account::test(id, AccountStatus::Active); + let mut account = crate::support::accounts::test_account(id, AccountStatus::Active); account.account_id = Some(format!("upstream-{id}")); account.plan_type = Some(plan_type.to_string()); account diff --git a/tests/upstream/protocol/usage_rate_limits.rs b/tests/upstream/protocol/usage_rate_limits.rs index 5d2af7db7..0f8a07a9b 100644 --- a/tests/upstream/protocol/usage_rate_limits.rs +++ b/tests/upstream/protocol/usage_rate_limits.rs @@ -361,7 +361,7 @@ fn cooldown_with_jitter_should_return_positive_duration_within_expected_range() async fn refresh_scheduler_should_refresh_before_expiry_and_preserve_refresh_token() { use async_trait::async_trait; use chrono::{Duration as ChronoDuration, Utc}; - use codex_proxy_rs::upstream::accounts::model::{Account, AccountStatus}; + use codex_proxy_rs::upstream::accounts::model::AccountStatus; use codex_proxy_rs::upstream::accounts::token_refresh::{ RefreshFailure, RefreshPolicy, RefreshScheduler, RefreshTrigger, TokenPair, TokenRefresher, }; @@ -379,7 +379,7 @@ async fn refresh_scheduler_should_refresh_before_expiry_and_preserve_refresh_tok } let now = Utc::now(); - let mut account = Account::test("acct_1", AccountStatus::Active); + let mut account = crate::support::accounts::test_account("acct_1", AccountStatus::Active); account.access_token_expires_at = Some(now + ChronoDuration::seconds(60)); account.refresh_token = Some("rt_keep".to_string()); let scheduler = RefreshScheduler::new( diff --git a/tests/upstream/transport/client.rs b/tests/upstream/transport/client.rs index de17ab78e..413ac30a7 100644 --- a/tests/upstream/transport/client.rs +++ b/tests/upstream/transport/client.rs @@ -3,7 +3,6 @@ use std::sync::{ Arc, }; -use codex_proxy_rs::upstream::fingerprint::Fingerprint; use codex_proxy_rs::upstream::protocol::responses::CodexResponsesRequest; use codex_proxy_rs::upstream::transport::{ CodexBackendClient, CodexRequestContext, CodexWebSocketPool, @@ -72,7 +71,7 @@ async fn codex_backend_client_should_apply_configured_websocket_pool() { let backend = CodexBackendClient::new( reqwest::Client::new(), format!("http://{addr}"), - Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(Arc::new(pool)); let mut request = CodexResponsesRequest::new_http_sse("gpt-5.5", "be brief", Vec::new()); diff --git a/tests/upstream/transport/headers.rs b/tests/upstream/transport/headers.rs index d271d5d2c..4acd48466 100644 --- a/tests/upstream/transport/headers.rs +++ b/tests/upstream/transport/headers.rs @@ -1,11 +1,10 @@ use super::*; -use codex_proxy_rs::upstream::fingerprint::Fingerprint; use codex_proxy_rs::upstream::transport::build_ordered_codex_headers; use codex_proxy_rs::upstream::transport::websocket::CodexWebSocketConnection; #[test] fn ordered_codex_headers_should_preserve_fingerprint_priority_and_request_fields() { - let fingerprint = Fingerprint::default_for_tests(); + let fingerprint = crate::support::fingerprint::test_fingerprint(); let headers = build_ordered_codex_headers( &fingerprint, @@ -279,7 +278,7 @@ async fn codex_backend_client_websocket_should_forward_security_chain_headers_an let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(pool); @@ -404,7 +403,7 @@ async fn codex_backend_client_should_send_desktop_headers_and_capture_response_m let client = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), server.uri(), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); let mut request = codex_proxy_rs::upstream::protocol::responses::CodexResponsesRequest::new_http_sse( @@ -470,7 +469,7 @@ async fn codex_backend_client_usage_should_use_wham_usage_headers() { let client = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), server.uri(), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); let usage = client @@ -550,7 +549,7 @@ async fn codex_backend_client_models_should_use_original_auxiliary_headers() { let client = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), server.uri(), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); let models = client @@ -615,7 +614,7 @@ async fn codex_backend_client_should_send_http_sse_headers_in_fingerprint_order( let client = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); client @@ -689,7 +688,7 @@ async fn codex_backend_client_should_send_compact_headers_in_fingerprint_order() let client = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); client diff --git a/tests/upstream/transport/http_client.rs b/tests/upstream/transport/http_client.rs index 2596987a9..d26e9c3cc 100644 --- a/tests/upstream/transport/http_client.rs +++ b/tests/upstream/transport/http_client.rs @@ -106,7 +106,7 @@ async fn codex_backend_client_probe_should_return_models_endpoint_status() { let client = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), server.uri(), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); let probe = client @@ -146,7 +146,7 @@ async fn codex_backend_client_should_cap_non_success_error_body_at_one_mib() { let client = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), server.uri(), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); let mut request = codex_proxy_rs::upstream::protocol::responses::CodexResponsesRequest::new_http_sse( diff --git a/tests/upstream/transport/websocket.rs b/tests/upstream/transport/websocket.rs index cd0635861..dd2e71770 100644 --- a/tests/upstream/transport/websocket.rs +++ b/tests/upstream/transport/websocket.rs @@ -100,7 +100,7 @@ async fn codex_backend_client_should_decode_permessage_deflate_context_takeover_ let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(pool); let mut request = @@ -1043,7 +1043,7 @@ async fn codex_backend_client_stream_should_reject_binary_websocket_event() { let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); let mut response = backend @@ -1099,7 +1099,7 @@ async fn codex_backend_client_stream_should_error_when_websocket_closes_before_t let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ); let mut response = backend @@ -1192,7 +1192,7 @@ async fn codex_backend_client_should_use_websocket_when_previous_response_id_is_ let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(pool); diff --git a/tests/upstream/transport/websocket_pool.rs b/tests/upstream/transport/websocket_pool.rs index a1f0fd766..2991a613d 100644 --- a/tests/upstream/transport/websocket_pool.rs +++ b/tests/upstream/transport/websocket_pool.rs @@ -54,7 +54,7 @@ async fn codex_backend_client_should_reuse_pooled_websocket_for_same_account_and let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(Arc::clone(&pool)); let mut request = @@ -143,7 +143,7 @@ async fn websocket_pool_should_bypass_busy_key_with_one_shot_connections() { let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(pool); let request = pooled_websocket_request("conversation-busy"); @@ -214,7 +214,7 @@ async fn websocket_pool_should_bypass_new_keys_after_account_cap() { let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(pool); let first_request = pooled_websocket_request("conversation-cap-one"); @@ -320,7 +320,7 @@ async fn codex_backend_client_should_ping_idle_pooled_websocket_during_maintenan let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(Arc::clone(&pool)); let mut request = @@ -393,7 +393,7 @@ async fn websocket_pool_should_evict_idle_connection_when_ping_times_out() { let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(Arc::clone(&pool)); let request = pooled_websocket_request("conversation-no-pong"); @@ -466,7 +466,7 @@ async fn websocket_pool_should_gc_expired_idle_connections() { let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(Arc::clone(&pool)); let request = pooled_websocket_request("conversation-gc"); @@ -569,7 +569,7 @@ async fn codex_backend_client_should_ping_idle_pooled_websocket_from_background_ let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(Arc::clone(&pool)); let mut request = @@ -674,7 +674,7 @@ async fn codex_backend_client_should_close_idle_pooled_websocket_when_account_is let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(Arc::clone(&pool)); let mut request = @@ -778,7 +778,7 @@ async fn codex_backend_client_should_stop_reusing_pooled_websockets_after_shutdo let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(Arc::clone(&pool)); let mut request = @@ -882,7 +882,7 @@ async fn codex_backend_client_should_close_idle_pooled_websocket_after_liveness_ let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(Arc::clone(&pool)); let mut request = @@ -977,7 +977,7 @@ async fn codex_backend_client_should_discard_pooled_websocket_after_upstream_err let backend = CodexBackendClient::new( reqwest::Client::builder().no_proxy().build().unwrap(), format!("http://{addr}"), - codex_proxy_rs::upstream::fingerprint::Fingerprint::default_for_tests(), + crate::support::fingerprint::test_fingerprint(), ) .with_websocket_pool(pool); let mut request =