test: move test helpers under tests

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-06-25 10:53:45 +08:00
co-authored by Codex
parent fe8834452b
commit 87ede4e1c4
40 changed files with 616 additions and 378 deletions
-40
View File
@@ -382,43 +382,3 @@ fn normalize_service_tier(service_tier: Option<&str>) -> Option<String> {
.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);
}
}
+8 -154
View File
@@ -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<DashboardTrendKind>,
}
#[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<u64> {
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<u64> {
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::<Vec<_>>();
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<String, f64> {
let mut records = usage_records.iter().collect::<Vec<_>>();
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<Utc>) -> DateTime<Utc> {
.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<Utc>, 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<Utc>,
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
}
}
+1 -43
View File
@@ -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<String>,
}
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 {
-5
View File
@@ -90,11 +90,6 @@ impl Fingerprint {
}
}
/// 返回测试用默认指纹。
pub fn default_for_tests() -> Self {
Self::default_codex_desktop()
}
/// 返回默认请求头集合。
pub fn default_headers() -> IndexMap<String, String> {
let mut headers = IndexMap::new();
+4 -4
View File
@@ -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,
+4 -4
View File
@@ -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,
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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,
+4 -4
View File
@@ -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,
+2 -2
View File
@@ -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,
+3 -3
View File
@@ -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,
+2 -2
View File
@@ -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,
+3 -3
View File
@@ -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,
+356
View File
@@ -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<Utc>, 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<Utc>, 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}"
);
}
+2 -2
View File
@@ -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,
+1
View File
@@ -1,2 +1,3 @@
pub mod dashboard;
pub mod events_store;
pub mod logs_routes;
+4 -4
View File
@@ -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,
+1 -1
View File
@@ -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,
+2 -2
View File
@@ -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 }
+3 -3
View File
@@ -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,
+2 -2
View File
@@ -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,
+6 -2
View File
@@ -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,
};
+9 -15
View File
@@ -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()),
+4 -1
View File
@@ -29,7 +29,10 @@ struct FakeAccountStore;
#[async_trait]
impl AccountStore for FakeAccountStore {
async fn list_pool_accounts(&self) -> AccountStoreResult<Vec<Account>> {
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(
+41
View File
@@ -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,
}
}
+11
View File
@@ -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
}
+2
View File
@@ -1,2 +1,4 @@
pub mod accounts;
pub mod config;
pub mod fingerprint;
pub mod sqlite;
+1 -1
View File
@@ -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,
+25 -13
View File
@@ -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;
@@ -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);
@@ -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",
+2 -3
View File
@@ -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));
+4 -4
View File
@@ -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");
+1 -1
View File
@@ -227,7 +227,7 @@ fn backend_models(model_id: &str) -> Vec<BackendModelEntry> {
}
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
+2 -2
View File
@@ -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(
+1 -2
View File
@@ -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());
+7 -8
View File
@@ -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
+2 -2
View File
@@ -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(
+4 -4
View File
@@ -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);
+11 -11
View File
@@ -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 =