mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-07 00:05:49 +00:00
test: move test helpers under tests
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
@@ -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,2 +1,3 @@
|
||||
pub mod dashboard;
|
||||
pub mod events_store;
|
||||
pub mod logs_routes;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,2 +1,4 @@
|
||||
pub mod accounts;
|
||||
pub mod config;
|
||||
pub mod fingerprint;
|
||||
pub mod sqlite;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
Reference in New Issue
Block a user