fix: hot reload fingerprint updates

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Zyy
2026-07-07 15:01:28 +08:00
co-authored by Codex
parent b32c4bcda8
commit 78f847fe2e
32 changed files with 434 additions and 174 deletions
+2 -1
View File
@@ -1033,7 +1033,8 @@ fn dashboard_usage_record_data(
}
fn service_status_data(state: &AppState) -> Vec<DashboardServiceStatusData> {
let fingerprint = fingerprint_diagnostics(&state.services.fingerprint);
let current_fingerprint = state.services.fingerprint.snapshot();
let fingerprint = fingerprint_diagnostics(&current_fingerprint);
vec![
DashboardServiceStatusData {
label: "客户端版本".to_string(),
+2 -1
View File
@@ -13,7 +13,7 @@ use crate::runtime::services::{BackgroundTaskStores, Services};
use crate::runtime::shutdown::{restart_executable_path, shutdown_signal};
use crate::runtime::state::{AppState, RuntimeConfig};
use crate::runtime::tasks::coordinator::TaskCoordinator;
use crate::upstream::fingerprint::{Fingerprint, FingerprintRepository};
use crate::upstream::fingerprint::{Fingerprint, FingerprintRepository, RuntimeFingerprint};
const RESTART_DELAY_ENV: &str = "CPR_RESTART_DELAY_MS";
@@ -81,6 +81,7 @@ async fn build_application(
let runtime_fingerprint = fingerprint_repository
.ensure_current_seed(&default_fingerprint)
.await?;
let runtime_fingerprint = RuntimeFingerprint::new(runtime_fingerprint);
let runtime_config = RuntimeConfig::from(config.clone());
let services = Services::try_with_installation_id(
&config,
+10 -6
View File
@@ -35,7 +35,7 @@ use crate::{
},
},
upstream::{
fingerprint::{Fingerprint, FingerprintRepository},
fingerprint::{FingerprintRepository, RuntimeFingerprint},
models::{
config::ModelConfig,
service::{ModelService, ModelServiceError},
@@ -98,7 +98,7 @@ pub struct Services {
pub session_affinity: StdArc<RuntimeSessionAffinityService>,
pub codex: StdArc<CodexBackendClient>,
pub websocket_pool: Option<StdArc<CodexWebSocketPool>>,
pub fingerprint: Fingerprint,
pub fingerprint: RuntimeFingerprint,
pub installation_id: Option<String>,
pub background_tasks: BackgroundTaskStores,
}
@@ -120,7 +120,11 @@ impl UsageRecordOptions {
}
impl Services {
pub fn new(config: &AppConfig, stores: BackgroundTaskStores, fingerprint: Fingerprint) -> Self {
pub fn new(
config: &AppConfig,
stores: BackgroundTaskStores,
fingerprint: RuntimeFingerprint,
) -> Self {
Self::try_new(config, stores, fingerprint)
.expect("failed to build runtime services with configured TLS transport")
}
@@ -128,7 +132,7 @@ impl Services {
pub fn try_new(
config: &AppConfig,
stores: BackgroundTaskStores,
fingerprint: Fingerprint,
fingerprint: RuntimeFingerprint,
) -> Result<Self, CustomCaError> {
Self::try_with_installation_id(config, stores, fingerprint, None)
}
@@ -136,7 +140,7 @@ impl Services {
pub(crate) fn try_with_installation_id(
config: &AppConfig,
stores: BackgroundTaskStores,
fingerprint: Fingerprint,
fingerprint: RuntimeFingerprint,
installation_id: Option<String>,
) -> Result<Self, CustomCaError> {
Self::try_with_installation_id_and_usage_record_options(
@@ -151,7 +155,7 @@ impl Services {
pub fn try_with_installation_id_and_usage_record_options(
config: &AppConfig,
stores: BackgroundTaskStores,
fingerprint: Fingerprint,
fingerprint: RuntimeFingerprint,
installation_id: Option<String>,
usage_record_options: UsageRecordOptions,
) -> Result<Self, CustomCaError> {
+8 -7
View File
@@ -86,16 +86,17 @@ impl TaskCoordinator {
.with_account_pool(services.account_pool.clone())
.start(),
);
coordinator.push(
"fingerprint_update",
coordinator.push("fingerprint_update", {
let fingerprint = services.fingerprint.snapshot();
start_fingerprint_update_task(
Some(stores.fingerprints.clone()),
stores.fingerprints.clone(),
services.fingerprint.clone(),
CODEX_DESKTOP_APPCAST_URL.to_string(),
PathBuf::from(DEFAULT_EXTRACTED_FINGERPRINT_PATH),
services.fingerprint.app_version.clone(),
services.fingerprint.build_number.clone(),
),
);
fingerprint.app_version,
fingerprint.build_number,
)
});
if let Some(pool) = &services.websocket_pool {
coordinator.push(
"websocket_pool",
@@ -2,9 +2,11 @@
use std::path::PathBuf;
use tracing::warn;
use tracing::{info, warn};
use crate::upstream::fingerprint::{FingerprintRepository, UpdateChecker, APPCAST_POLL_INTERVAL};
use crate::upstream::fingerprint::{
FingerprintRepository, RuntimeFingerprint, UpdateChecker, APPCAST_POLL_INTERVAL,
};
use super::{
coordinator::SchedulerHandle,
@@ -19,7 +21,8 @@ pub const DEFAULT_EXTRACTED_FINGERPRINT_PATH: &str = "data/extracted-fingerprint
/// 启动指纹自动更新后台任务。
pub fn start_fingerprint_update_task(
repository: Option<FingerprintRepository>,
repository: FingerprintRepository,
runtime_fingerprint: RuntimeFingerprint,
appcast_url: String,
extracted_fingerprint_path: PathBuf,
current_version: String,
@@ -36,6 +39,7 @@ pub fn start_fingerprint_update_task(
spawn_periodic_task(
FingerprintUpdateTask {
checker: update_checker,
runtime_fingerprint,
first_tick: true,
},
PeriodicTaskConfig::new(
@@ -48,6 +52,7 @@ pub fn start_fingerprint_update_task(
struct FingerprintUpdateTask {
checker: UpdateChecker,
runtime_fingerprint: RuntimeFingerprint,
first_tick: bool,
}
@@ -55,11 +60,24 @@ impl PeriodicTaskRunner for FingerprintUpdateTask {
fn tick(&mut self) -> super::periodic::TaskFuture<'_, ()> {
Box::pin(async move {
let first_tick = std::mem::take(&mut self.first_tick);
if let Err(error) = self.checker.check_and_apply_update().await {
if first_tick {
warn!(error = %error, "fingerprint 首次版本检查失败");
} else {
warn!(error = %error, "fingerprint 定期版本检查失败");
match self.checker.check_and_apply_update().await {
Ok(Some(fingerprint)) => {
let app_version = fingerprint.app_version.clone();
let build_number = fingerprint.build_number.clone();
self.runtime_fingerprint.replace(fingerprint);
info!(
app_version = %app_version,
build_number = %build_number,
"fingerprint 运行时版本已更新"
);
}
Ok(None) => {}
Err(error) => {
if first_tick {
warn!(error = %error, "fingerprint 首次版本检查失败");
} else {
warn!(error = %error, "fingerprint 定期版本检查失败");
}
}
}
})
+61 -19
View File
@@ -1,6 +1,10 @@
//! 指纹类型、更新与持久化。
use std::{path::PathBuf, sync::Arc, time::Duration};
use std::{
path::PathBuf,
sync::{Arc, RwLock},
time::Duration,
};
use chrono::{DateTime, Utc};
use indexmap::IndexMap;
@@ -89,6 +93,38 @@ impl Fingerprint {
}
}
/// 运行时共享指纹快照。
#[derive(Debug, Clone)]
pub struct RuntimeFingerprint {
current: Arc<RwLock<Arc<Fingerprint>>>,
}
impl RuntimeFingerprint {
pub fn new(fingerprint: Fingerprint) -> Self {
Self {
current: Arc::new(RwLock::new(Arc::new(fingerprint))),
}
}
pub fn current(&self) -> Arc<Fingerprint> {
self.current
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
pub fn snapshot(&self) -> Fingerprint {
self.current().as_ref().clone()
}
pub fn replace(&self, fingerprint: Fingerprint) {
*self
.current
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Arc::new(fingerprint);
}
}
// ---------------------------------------------------------------------------
// 指纹历史记录与仓储
// ---------------------------------------------------------------------------
@@ -345,7 +381,7 @@ fn json_error(error: serde_json::Error) -> sqlx::Error {
/// Appcast 指纹更新检查器。
#[derive(Clone)]
pub struct UpdateChecker {
repository: Option<FingerprintRepository>,
repository: FingerprintRepository,
client: reqwest::Client,
appcast_url: String,
extracted_fingerprint_path: PathBuf,
@@ -360,7 +396,7 @@ struct InternalState {
impl UpdateChecker {
/// 使用自定义 client 和路径构造检查器。
pub fn with_client(
repository: Option<FingerprintRepository>,
repository: FingerprintRepository,
client: reqwest::Client,
appcast_url: impl Into<String>,
extracted_fingerprint_path: PathBuf,
@@ -406,18 +442,18 @@ impl UpdateChecker {
}
/// 检查并在需要时应用更新。
pub async fn check_and_apply_update(&self) -> Result<bool, UpdateError> {
pub async fn check_and_apply_update(&self) -> Result<Option<Fingerprint>, UpdateError> {
let update_state = self.check_for_update().await?;
if !update_state.update_available {
return Ok(false);
return Ok(None);
}
let (Some(version), Some(build)) = (
update_state.latest_version.as_deref(),
update_state.latest_build.as_deref(),
) else {
return Ok(false);
return Ok(None);
};
let state = self.state.lock().await;
@@ -433,13 +469,13 @@ impl UpdateChecker {
"发现新的 fingerprint 版本"
);
self.apply_version_update(version, build).await?;
let updated_fingerprint = self.apply_version_update(version, build).await?;
let mut state = self.state.lock().await;
state.current_version = version.to_string();
state.current_build = build.to_string();
Ok(true)
Ok(Some(updated_fingerprint))
}
async fn fetch_appcast(&self) -> Result<String, UpdateError> {
@@ -457,20 +493,26 @@ impl UpdateChecker {
response.text().await.map_err(UpdateError::from)
}
async fn apply_version_update(&self, version: &str, build: &str) -> Result<(), UpdateError> {
async fn apply_version_update(
&self,
version: &str,
build: &str,
) -> Result<Fingerprint, UpdateError> {
let chromium_version =
load_matching_chromium_version(&self.extracted_fingerprint_path, version, build);
if let Some(repository) = &self.repository {
repository
.update_current_version(version, build, chromium_version.as_deref())
.await?;
repository
.insert_update_history(version, build, chromium_version.as_deref(), None)
.await?;
}
Ok(())
self.repository
.update_current_version(version, build, chromium_version.as_deref())
.await?;
self.repository
.insert_update_history(version, build, chromium_version.as_deref(), None)
.await?;
let updated = self
.repository
.load_current()
.await?
.ok_or(sqlx::Error::RowNotFound)?;
Ok(updated)
}
}
+20 -14
View File
@@ -22,7 +22,7 @@ use thiserror::Error;
use tokio_tungstenite::tungstenite::handshake::client::generate_key;
use uuid::Uuid;
use crate::upstream::fingerprint::Fingerprint;
use crate::upstream::fingerprint::{Fingerprint, RuntimeFingerprint};
use crate::upstream::models::backend_entry::BackendModelEntry;
use crate::upstream::protocol::events::{extract_sse_usage, retry_after_seconds_from_body};
use crate::upstream::protocol::responses::{
@@ -333,14 +333,18 @@ pub struct CodexCompactResponse {
pub struct CodexBackendClient {
pub(super) client: Client,
pub(super) base_url: String,
fingerprint: Fingerprint,
fingerprint: RuntimeFingerprint,
websocket_pool: Option<Arc<CodexWebSocketPool>>,
websocket_first_token_timeout: Option<Duration>,
}
impl CodexBackendClient {
/// 构造客户端。
pub fn new(client: Client, base_url: impl Into<String>, fingerprint: Fingerprint) -> Self {
pub fn new(
client: Client,
base_url: impl Into<String>,
fingerprint: RuntimeFingerprint,
) -> Self {
Self {
client,
base_url: base_url.into().trim_end_matches('/').to_string(),
@@ -798,17 +802,18 @@ impl CodexBackendClient {
&self,
context: CodexRequestContext<'_>,
) -> CodexClientResult<Vec<BackendModelEntry>> {
let fingerprint = self.fingerprint.current();
let endpoints = [
format!(
"{}/codex/models?client_version={}",
self.base_url, self.fingerprint.app_version
self.base_url, fingerprint.app_version
),
format!("{}/models", self.base_url),
format!("{}/sentinel/chat-requirements", self.base_url),
];
for endpoint in endpoints {
let headers = self.auxiliary_request_headers(context)?;
let headers = self.auxiliary_request_headers(&fingerprint, context)?;
let response = self.client.get(endpoint).headers(headers).send().await?;
if !response.status().is_success() {
continue;
@@ -845,9 +850,10 @@ impl CodexBackendClient {
}
fn request_headers(&self, context: CodexRequestContext<'_>) -> CodexClientResult<HeaderMap> {
let fingerprint = self.fingerprint.current();
let request_id = context.session_id.unwrap_or(context.request_id);
let ordered_headers = build_ordered_codex_base_headers(
&self.fingerprint,
&fingerprint,
context.access_token,
context.account_id,
);
@@ -897,13 +903,11 @@ impl CodexBackendClient {
fn auxiliary_request_headers(
&self,
fingerprint: &Fingerprint,
context: CodexRequestContext<'_>,
) -> CodexClientResult<HeaderMap> {
let ordered_headers = build_ordered_codex_base_headers(
&self.fingerprint,
context.access_token,
context.account_id,
);
let ordered_headers =
build_ordered_codex_base_headers(fingerprint, context.access_token, context.account_id);
let mut headers = HeaderMap::new();
insert_ordered_headers(&mut headers, &ordered_headers)?;
@@ -924,10 +928,11 @@ impl CodexBackendClient {
&self,
context: CodexRequestContext<'_>,
) -> CodexClientResult<HeaderMap> {
let fingerprint = self.fingerprint.current();
let mut headers = HeaderMap::new();
headers.insert(
USER_AGENT,
HeaderValue::from_str(&self.fingerprint.user_agent())?,
HeaderValue::from_str(&fingerprint.user_agent())?,
);
headers.insert(
AUTHORIZATION,
@@ -935,7 +940,7 @@ impl CodexBackendClient {
);
headers.insert(
HeaderName::from_static("originator"),
HeaderValue::from_str(&self.fingerprint.originator)?,
HeaderValue::from_str(&fingerprint.originator)?,
);
insert_optional_header(&mut headers, "chatgpt-account-id", context.account_id)?;
insert_optional_header(&mut headers, "cookie", context.cookie_header)?;
@@ -947,8 +952,9 @@ impl CodexBackendClient {
&self,
context: CodexRequestContext<'_>,
) -> CodexClientResult<HeaderMap> {
let fingerprint = self.fingerprint.current();
let ordered_headers = build_ordered_codex_base_headers(
&self.fingerprint,
&fingerprint,
context.access_token,
context.account_id,
);
+15 -3
View File
@@ -30,7 +30,11 @@ async fn admin_accounts_import_should_store_cpr_account_tokens() {
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = Arc::new(Services::new(&config, stores, fingerprint));
let services = Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
@@ -639,7 +643,11 @@ async fn admin_accounts_import_should_fetch_usage_to_complete_missing_plan_and_q
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = Arc::new(Services::new(&config, stores, fingerprint));
let services = Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
@@ -855,7 +863,11 @@ async fn admin_accounts_import_should_require_admin_session_cookie() {
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = Arc::new(Services::new(&config, stores, fingerprint));
let services = Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
+15 -3
View File
@@ -23,7 +23,11 @@ async fn admin_accounts_lifecycle_should_update_and_delete_accounts() {
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = Arc::new(Services::new(&config, stores, fingerprint));
let services = Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
@@ -748,7 +752,11 @@ async fn admin_account_create_should_derive_claims_and_store_plain_tokens() {
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = Arc::new(Services::new(&config, stores, fingerprint));
let services = Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
@@ -791,7 +799,11 @@ async fn admin_account_manual_create_should_reject_missing_invalid_expired_or_un
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = Arc::new(Services::new(&config, stores, fingerprint));
let services = Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
+5 -1
View File
@@ -27,7 +27,11 @@ async fn admin_accounts_list_should_not_expose_account_tokens() {
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = Arc::new(Services::new(&config, stores, fingerprint));
let services = Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
+9 -2
View File
@@ -26,7 +26,10 @@ use sqlx::SqlitePool;
use tower::util::ServiceExt;
use crate::support::jwt::unsigned_jwt;
use crate::support::{admin::seed_admin_session, config::test_config, http::response_json};
use crate::support::{
admin::seed_admin_session, config::test_config, fingerprint::runtime_fingerprint,
http::response_json,
};
mod exporting;
mod importing;
@@ -170,7 +173,11 @@ async fn admin_accounts_test_app_with_overrides(
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = Arc::new(Services::new(&config, stores, fingerprint));
let services = Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
+2 -2
View File
@@ -7,7 +7,7 @@ use codex_proxy_rs::infra::{database::connect_sqlite, identity::hash_admin_passw
use sqlx::SqlitePool;
use tower::util::ServiceExt;
use crate::support::{config::test_config, http::response_json};
use crate::support::{config::test_config, fingerprint::runtime_fingerprint, http::response_json};
#[tokio::test]
async fn admin_login_should_issue_http_only_session_cookie() {
@@ -202,7 +202,7 @@ async fn admin_login_test_app(
let services = std::sync::Arc::new(codex_proxy_rs::runtime::services::Services::new(
&config,
stores,
fingerprint,
runtime_fingerprint(fingerprint),
));
let state = codex_proxy_rs::runtime::state::AppState {
services: (*services).clone(),
+9 -2
View File
@@ -17,7 +17,10 @@ use codex_proxy_rs::{
use serde_json::json;
use tower::util::ServiceExt;
use crate::support::{admin::seed_admin_session, config::test_config, http::response_json};
use crate::support::{
admin::seed_admin_session, config::test_config, fingerprint::runtime_fingerprint,
http::response_json,
};
mod authorization;
mod lifecycle;
@@ -41,7 +44,11 @@ async fn admin_client_key_test_app(db_name: &str) -> (axum::Router, tempfile::Te
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint));
let services = std::sync::Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
+8 -2
View File
@@ -32,7 +32,9 @@ use serde_json::{json, Value};
use sqlx::SqlitePool;
use tower::util::ServiceExt;
use crate::support::{admin::seed_admin_session, http::response_json};
use crate::support::{
admin::seed_admin_session, fingerprint::runtime_fingerprint, http::response_json,
};
#[tokio::test]
async fn dashboard_summary_should_render_fingerprint_updated_at_as_beijing_time() {
@@ -654,7 +656,11 @@ async fn dashboard_test_app(
client_keys: SqliteClientKeyStore::new(pool.clone()),
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint));
let services = std::sync::Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
@@ -23,7 +23,10 @@ use serde_json::json;
use sqlx::SqlitePool;
use tower::util::ServiceExt;
use crate::support::{admin::seed_admin_session, config::test_config, http::response_json};
use crate::support::{
admin::seed_admin_session, config::test_config, fingerprint::runtime_fingerprint,
http::response_json,
};
#[tokio::test]
async fn admin_usage_records_should_require_admin_session_cookie() {
@@ -883,7 +886,11 @@ async fn admin_usage_records_test_app_with_config(
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint));
let services = std::sync::Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
+18 -5
View File
@@ -28,7 +28,8 @@ use sqlx::SqlitePool;
use tower::util::ServiceExt;
use crate::support::{
admin::seed_admin_session, config::test_config as base_test_config, http::response_json,
admin::seed_admin_session, config::test_config as base_test_config,
fingerprint::runtime_fingerprint, http::response_json,
};
#[tokio::test]
@@ -277,7 +278,7 @@ async fn admin_settings_update_should_persist_runtime_settings_to_database() {
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = Services::new(&config, stores, fingerprint);
let services = Services::new(&config, stores, runtime_fingerprint(fingerprint));
let services = std::sync::Arc::new(services);
let state = AppState {
services: (*services).clone(),
@@ -382,7 +383,11 @@ async fn admin_settings_update_should_apply_runtime_services() {
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint));
let services = std::sync::Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
services
.account_pool
.restore_from_repository()
@@ -480,7 +485,11 @@ async fn admin_settings_update_should_reject_unsupported_or_invalid_fields() {
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint));
let services = std::sync::Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
@@ -522,7 +531,11 @@ async fn admin_settings_test_app(db_name: &str) -> (axum::Router, tempfile::Temp
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint));
let services = std::sync::Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
let state = AppState {
services: (*services).clone(),
};
+8 -2
View File
@@ -35,7 +35,9 @@ use wiremock::{
};
use crate::support::{
admin::seed_admin_session, config::test_config, fingerprint::test_fingerprint,
admin::seed_admin_session,
config::test_config,
fingerprint::{runtime_fingerprint, test_fingerprint},
http::response_json,
};
@@ -1218,7 +1220,11 @@ async fn admin_system_test_app(db_name: &str) -> (axum::Router, tempfile::TempDi
seed_admin_session(&pool, "session_1").await;
let config = test_config(url);
let stores = stores(pool);
let services = Arc::new(Services::new(&config, stores, test_fingerprint()));
let services = Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(test_fingerprint()),
));
let state = AppState {
services: (*services).clone(),
};
+4 -2
View File
@@ -10,7 +10,9 @@ use serde_json::Value;
use sqlx::SqlitePool;
use tower::ServiceExt;
use crate::support::{config::test_config as base_test_config, http::response_json};
use crate::support::{
config::test_config as base_test_config, fingerprint::runtime_fingerprint, http::response_json,
};
#[tokio::test]
async fn server_router_should_serve_frontend_assets_without_shadowing_api_routes() {
@@ -200,7 +202,7 @@ async fn router_with_dist_and_database(dist: &Path, database_url: &str) -> (Rout
let services = std::sync::Arc::new(codex_proxy_rs::runtime::services::Services::new(
&config,
stores,
fingerprint,
runtime_fingerprint(fingerprint),
));
let state = codex_proxy_rs::runtime::state::AppState {
services: (*services).clone(),
@@ -271,7 +271,7 @@ async fn test_app_state_with_pool_installation_id_and_usage_record_options(
let services = Services::try_with_installation_id_and_usage_record_options(
&config,
stores,
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
Some(installation_id),
usage_record_options,
)
+7 -2
View File
@@ -20,7 +20,8 @@ use codex_proxy_rs::{
use tower::util::ServiceExt;
use crate::support::{
client_keys::insert_client_api_key, config::test_config, http::response_json,
client_keys::insert_client_api_key, config::test_config, fingerprint::runtime_fingerprint,
http::response_json,
};
#[tokio::test]
@@ -164,7 +165,11 @@ async fn build_test_app(
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint));
let services = std::sync::Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
services
.initialize_hot_path_state()
.await
@@ -18,7 +18,8 @@ use sqlx::SqlitePool;
use tower::util::ServiceExt;
use crate::support::{
client_keys::insert_client_api_key, config::test_config, http::response_json,
client_keys::insert_client_api_key, config::test_config, fingerprint::runtime_fingerprint,
http::response_json,
};
#[tokio::test]
@@ -154,7 +155,11 @@ async fn test_app_with_client_api_key() -> (axum::Router, String, tempfile::Temp
usage_records: SqliteUsageRecordStore::new(pool.clone()),
};
let fingerprint = crate::support::fingerprint::test_fingerprint();
let services = std::sync::Arc::new(Services::new(&config, stores, fingerprint));
let services = std::sync::Arc::new(Services::new(
&config,
stores,
runtime_fingerprint(fingerprint),
));
services
.initialize_hot_path_state()
.await
+3 -3
View File
@@ -49,7 +49,7 @@ async fn services_try_new_should_use_configured_tls_transport_builder() {
let Err(error) = Services::try_new(
&config,
stores,
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
) else {
panic!("invalid custom CA should fail service transport construction");
};
@@ -90,7 +90,7 @@ async fn services_try_new_should_expose_websocket_pool_when_enabled() {
let services = Services::try_new(
&config,
stores(pool),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.expect("services should build");
@@ -111,7 +111,7 @@ async fn services_try_new_should_not_expose_websocket_pool_when_disabled() {
let services = Services::try_new(
&config,
stores(pool),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.expect("services should build");
+18 -33
View File
@@ -11,9 +11,11 @@ async fn fingerprint_update_task_should_start_background_checker() {
repo.ensure_current_seed(&crate::support::fingerprint::test_fingerprint())
.await
.expect("seed current fingerprint");
let runtime_fingerprint = runtime_fingerprint_from_repo(&repo).await;
let handle = codex_proxy_rs::runtime::tasks::fingerprint_update::start_fingerprint_update_task(
Some(repo),
repo,
runtime_fingerprint,
"https://example.invalid/appcast.xml".to_string(),
dir.path().join("extracted-fingerprint.json"),
"26.800.1".to_string(),
@@ -49,6 +51,7 @@ async fn fingerprint_update_task_should_apply_initial_appcast_update_to_reposito
repo.ensure_current_seed(&crate::support::fingerprint::test_fingerprint())
.await
.expect("seed current fingerprint");
let runtime_fingerprint = runtime_fingerprint_from_repo(&repo).await;
let extracted_path = dir.path().join("extracted-fingerprint.json");
std::fs::write(
&extracted_path,
@@ -57,7 +60,8 @@ async fn fingerprint_update_task_should_apply_initial_appcast_update_to_reposito
.expect("extracted fingerprint should be written");
let handle = codex_proxy_rs::runtime::tasks::fingerprint_update::start_fingerprint_update_task(
Some(repo.clone()),
repo.clone(),
runtime_fingerprint.clone(),
format!("{}/appcast.xml", server.uri()),
extracted_path,
"26.800.1".to_string(),
@@ -75,6 +79,15 @@ async fn fingerprint_update_task_should_apply_initial_appcast_update_to_reposito
),
("26.900.1", "7001", "147")
);
let runtime_snapshot = runtime_fingerprint.snapshot();
assert_eq!(
(
runtime_snapshot.app_version.as_str(),
runtime_snapshot.build_number.as_str(),
runtime_snapshot.chromium_version.as_str()
),
("26.900.1", "7001", "147")
);
}
#[tokio::test]
@@ -103,8 +116,10 @@ async fn fingerprint_update_task_should_not_persist_when_appcast_matches_current
repo.ensure_current_seed(&crate::support::fingerprint::test_fingerprint())
.await
.expect("seed current fingerprint");
let runtime_fingerprint = runtime_fingerprint_from_repo(&repo).await;
let handle = codex_proxy_rs::runtime::tasks::fingerprint_update::start_fingerprint_update_task(
Some(repo.clone()),
repo.clone(),
runtime_fingerprint,
format!("{}/appcast.xml", server.uri()),
dir.path().join("missing-extracted-fingerprint.json"),
"26.900.1".to_string(),
@@ -120,33 +135,3 @@ async fn fingerprint_update_task_should_not_persist_when_appcast_matches_current
assert_eq!(history_count, 0);
}
#[tokio::test]
async fn fingerprint_update_task_should_check_appcast_without_repository() {
let server = MockServer::start().await;
mount_appcast(
&server,
r#"
<rss>
<channel>
<item>
<enclosure url="https://example.invalid/download" sparkle:shortVersionString="26.900.1" sparkle:version="7001" />
</item>
</channel>
</rss>
"#,
)
.await;
let dir = tempfile::tempdir().expect("temp dir");
let handle = codex_proxy_rs::runtime::tasks::fingerprint_update::start_fingerprint_update_task(
None,
format!("{}/appcast.xml", server.uri()),
dir.path().join("missing-extracted-fingerprint.json"),
"26.800.1".to_string(),
"6001".to_string(),
);
wait_for_appcast_requests(&server, 1).await;
handle.shutdown().await;
}
+10 -1
View File
@@ -10,7 +10,7 @@ use codex_proxy_rs::upstream::accounts::cookies::SqliteCookieStore;
use codex_proxy_rs::upstream::accounts::model::{Account, AccountStatus};
use codex_proxy_rs::upstream::accounts::model::{AccountModelUsageDelta, AccountUsageDelta};
use codex_proxy_rs::upstream::accounts::store::{AccountStore, AccountStoreResult};
use codex_proxy_rs::upstream::fingerprint::FingerprintRepository;
use codex_proxy_rs::upstream::fingerprint::{FingerprintRepository, RuntimeFingerprint};
use codex_proxy_rs::upstream::models::config::ModelConfig;
use codex_proxy_rs::upstream::models::service::ModelService;
use wiremock::{
@@ -137,6 +137,15 @@ async fn wait_for_current_fingerprint_version(
}
}
async fn runtime_fingerprint_from_repo(repo: &FingerprintRepository) -> RuntimeFingerprint {
RuntimeFingerprint::new(
repo.load_current()
.await
.expect("current fingerprint should load")
.expect("current fingerprint should exist"),
)
}
async fn wait_for_appcast_requests(server: &MockServer, expected_count: usize) {
let deadline = tokio::time::Instant::now() + StdDuration::from_secs(2);
loop {
+12 -1
View File
@@ -1,4 +1,7 @@
use codex_proxy_rs::{config::schema::FingerprintConfig, upstream::fingerprint::Fingerprint};
use codex_proxy_rs::{
config::schema::FingerprintConfig,
upstream::fingerprint::{Fingerprint, RuntimeFingerprint},
};
pub(crate) fn test_fingerprint() -> Fingerprint {
Fingerprint::from_config(&FingerprintConfig::default())
@@ -9,3 +12,11 @@ pub(crate) fn test_fingerprint_with_updated_at(updated_at: Option<&str>) -> Fing
fingerprint.updated_at = updated_at.map(ToString::to_string);
fingerprint
}
pub(crate) fn runtime_test_fingerprint() -> RuntimeFingerprint {
RuntimeFingerprint::new(test_fingerprint())
}
pub(crate) fn runtime_fingerprint(fingerprint: Fingerprint) -> RuntimeFingerprint {
RuntimeFingerprint::new(fingerprint)
}
@@ -312,7 +312,7 @@ async fn quota_refresh_service_should_send_usage_cookie_when_cookie_store_is_con
let codex = CodexBackendClient::new(
reqwest::Client::new(),
server.uri(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let service =
RuntimeQuotaRefreshService::new(store, Arc::new(codex)).with_cookie_store(cookies);
@@ -393,7 +393,7 @@ async fn quota_refresh_service_should_fetch_usage_for_quota_locked_accounts_and_
let codex = CodexBackendClient::new(
reqwest::Client::new(),
server.uri(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let service = RuntimeQuotaRefreshService::new(store.clone(), Arc::new(codex));
@@ -468,7 +468,7 @@ async fn quota_refresh_service_should_refresh_quota_exhausted_accounts() {
let codex = CodexBackendClient::new(
reqwest::Client::new(),
server.uri(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let service = RuntimeQuotaRefreshService::new(store, Arc::new(codex));
@@ -505,7 +505,7 @@ async fn quota_refresh_service_should_skip_recent_locked_account_before_cooldown
let codex = CodexBackendClient::new(
reqwest::Client::new(),
server.uri(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let service =
RuntimeQuotaRefreshService::with_min_refresh_interval_secs(store, Arc::new(codex), 1_800);
@@ -546,7 +546,7 @@ async fn quota_refresh_service_should_skip_recent_locked_account_inside_cooldown
let codex = CodexBackendClient::new(
reqwest::Client::new(),
server.uri(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let service =
RuntimeQuotaRefreshService::with_min_refresh_interval_secs(store, Arc::new(codex), 1_800);
@@ -604,7 +604,7 @@ async fn quota_refresh_service_should_bypass_recent_skip_after_cooldown_grace()
let codex = CodexBackendClient::new(
reqwest::Client::new(),
server.uri(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let service =
RuntimeQuotaRefreshService::with_min_refresh_interval_secs(store, Arc::new(codex), 1_800);
@@ -658,7 +658,7 @@ async fn quota_refresh_service_should_stagger_multiple_locked_account_requests()
let codex = CodexBackendClient::new(
reqwest::Client::new(),
server.uri(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let service = RuntimeQuotaRefreshService::new(store, Arc::new(codex))
.with_request_spacing(StdDuration::from_millis(200));
+19 -9
View File
@@ -57,14 +57,21 @@ async fn update_checker_should_report_available_update_from_appcast() {
.mount(&server)
.await;
let dir = tempfile::tempdir().expect("temp dir");
let db = dir.path().join("fingerprints-check.sqlite");
let pool = connect_sqlite(&format!("sqlite://{}", db.display()))
.await
.expect("sqlite pool");
let repo = FingerprintRepository::new(pool);
repo.ensure_current_seed(&crate::support::fingerprint::test_fingerprint())
.await
.expect("seed current fingerprint");
let checker = UpdateChecker::with_client(
None,
repo,
reqwest::Client::new(),
format!("{}/appcast.xml", server.uri()),
tempfile::tempdir()
.expect("temp dir")
.path()
.join("extracted-fingerprint.json"),
dir.path().join("extracted-fingerprint.json"),
"26.800.1",
"6001",
);
@@ -113,7 +120,7 @@ async fn update_checker_should_apply_available_update_to_repository() {
.expect("write extracted fingerprint");
let checker = UpdateChecker::with_client(
Some(repo.clone()),
repo.clone(),
reqwest::Client::new(),
format!("{}/appcast.xml", server.uri()),
extracted_path,
@@ -121,17 +128,20 @@ async fn update_checker_should_apply_available_update_to_repository() {
"6001",
);
let applied = checker
let updated = checker
.check_and_apply_update()
.await
.expect("apply update");
.expect("apply update")
.expect("available update should return current fingerprint");
let stored = repo
.load_current()
.await
.expect("load current")
.expect("stored fingerprint");
assert!(applied);
assert_eq!(updated.app_version, "26.900.1");
assert_eq!(updated.build_number, "7001");
assert_eq!(updated.chromium_version, "147");
assert_eq!(stored.app_version, "26.900.1");
assert_eq!(stored.build_number, "7001");
assert_eq!(stored.chromium_version, "147");
+1 -1
View File
@@ -59,7 +59,7 @@ async fn codex_backend_client_should_apply_configured_websocket_pool() {
let backend = CodexBackendClient::new(
reqwest::Client::new(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::new(pool));
let mut request = CodexResponsesRequest::new_http_sse("gpt-5.5", "be brief", Vec::new());
+92 -6
View File
@@ -1,4 +1,5 @@
use super::*;
use codex_proxy_rs::upstream::fingerprint::RuntimeFingerprint;
use codex_proxy_rs::upstream::transport::build_ordered_codex_headers;
use codex_proxy_rs::upstream::transport::websocket::CodexWebSocketConnection;
use serde_json::Value;
@@ -279,7 +280,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(pool);
@@ -420,7 +421,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(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let mut request =
codex_proxy_rs::upstream::protocol::responses::CodexResponsesRequest::new_http_sse(
@@ -471,6 +472,91 @@ async fn codex_backend_client_should_send_desktop_headers_and_capture_response_m
);
}
#[tokio::test]
async fn codex_backend_client_should_use_latest_runtime_fingerprint_for_new_requests() {
let server = wiremock::MockServer::start().await;
let sse_body = include_str!("../../fixtures/responses/http_sse/completed_usage_basic.sse");
wiremock::Mock::given(wiremock::matchers::method("POST"))
.and(wiremock::matchers::path("/codex/responses"))
.respond_with(
wiremock::ResponseTemplate::new(200)
.insert_header("content-type", "text/event-stream")
.set_body_string(sse_body),
)
.mount(&server)
.await;
let mut initial = crate::support::fingerprint::test_fingerprint();
initial.app_version = "26.800.1".to_string();
let runtime_fingerprint = RuntimeFingerprint::new(initial);
let client = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
server.uri(),
runtime_fingerprint.clone(),
);
let mut request =
codex_proxy_rs::upstream::protocol::responses::CodexResponsesRequest::new_http_sse(
"gpt-5.5",
"",
Vec::new(),
);
request.force_http_sse = true;
let request_context = |request_id| CodexRequestContext {
access_token: "access-token",
account_id: Some("chatgpt-account"),
request_id,
turn_state: None,
turn_metadata: None,
beta_features: None,
include_timing_metrics: None,
version: None,
codex_window_id: None,
parent_thread_id: None,
cookie_header: None,
installation_id: None,
session_id: None,
};
client
.create_response(&request, request_context("req_fingerprint_1"))
.await
.expect("first response should succeed");
let mut updated = runtime_fingerprint.snapshot();
updated.app_version = "26.900.1".to_string();
updated.build_number = "7001".to_string();
runtime_fingerprint.replace(updated);
client
.create_response(&request, request_context("req_fingerprint_2"))
.await
.expect("second response should succeed");
let user_agents = server
.received_requests()
.await
.expect("received requests should load")
.iter()
.map(|request| {
request
.headers
.get("user-agent")
.expect("user-agent should be sent")
.to_str()
.expect("user-agent should be valid")
.to_string()
})
.collect::<Vec<_>>();
assert_eq!(
user_agents,
vec![
"Codex Desktop/26.800.1 (darwin; arm64)".to_string(),
"Codex Desktop/26.900.1 (darwin; arm64)".to_string(),
]
);
}
#[tokio::test]
async fn codex_backend_client_usage_should_use_wham_usage_headers() {
let server = wiremock::MockServer::start().await;
@@ -486,7 +572,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(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let usage = client
@@ -571,7 +657,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(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let models = client
@@ -638,7 +724,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
client
@@ -713,7 +799,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
client
@@ -107,7 +107,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(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let mut request =
codex_proxy_rs::upstream::protocol::responses::CodexResponsesRequest::new_http_sse(
@@ -161,7 +161,7 @@ async fn codex_backend_client_should_parse_retry_after_from_rate_limit_error_bod
let client = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
server.uri(),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let mut request =
codex_proxy_rs::upstream::protocol::responses::CodexResponsesRequest::new_http_sse(
@@ -107,7 +107,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(pool);
let mut request =
@@ -1000,7 +1000,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let Err(error) = backend
@@ -1073,7 +1073,7 @@ async fn codex_backend_client_stream_should_retry_without_pool_when_first_conten
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_first_token_timeout(Some(Duration::from_millis(30)));
let request = pooled_websocket_request("conversation-first-token-no-pool-timeout");
@@ -1133,7 +1133,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let mut response = backend
@@ -1203,7 +1203,7 @@ async fn codex_backend_client_stream_should_wait_for_terminal_after_active_webso
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let mut response = backend
@@ -1267,7 +1267,7 @@ async fn codex_backend_client_stream_should_timeout_when_active_websocket_stalls
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
);
let mut response = backend
@@ -1363,7 +1363,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(pool);
@@ -39,7 +39,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let mut request =
@@ -117,7 +117,7 @@ async fn codex_backend_client_should_open_fresh_socket_when_idle_pooled_websocke
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let mut request =
@@ -212,7 +212,7 @@ async fn codex_backend_client_stream_should_retry_fresh_socket_when_first_conten
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let request = pooled_websocket_request("conversation-first-token-fresh-timeout");
@@ -305,7 +305,7 @@ async fn codex_backend_client_stream_should_retry_reused_socket_when_first_conte
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let request = pooled_websocket_request("conversation-first-token-reuse-timeout");
@@ -399,7 +399,7 @@ async fn codex_backend_client_stream_should_retry_bypass_socket_when_first_conte
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let request = pooled_websocket_request("conversation-first-token-bypass-timeout");
@@ -454,7 +454,7 @@ async fn codex_backend_client_should_not_reuse_pooled_websocket_across_local_acc
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let mut request =
@@ -547,7 +547,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(pool);
let request = pooled_websocket_request("conversation-busy");
@@ -640,7 +640,7 @@ async fn websocket_pool_should_release_slot_when_client_drops_stream() {
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(pool);
let request = pooled_websocket_request("conversation-drop");
@@ -714,7 +714,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(pool);
let first_request = pooled_websocket_request("conversation-cap-one");
@@ -800,7 +800,7 @@ async fn websocket_pool_should_evict_idle_connection_when_liveness_lapses_despit
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let request = pooled_websocket_request("conversation-no-pong");
@@ -874,7 +874,7 @@ async fn websocket_pool_should_gc_expired_idle_connections() {
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let request = pooled_websocket_request("conversation-gc");
@@ -978,7 +978,7 @@ async fn codex_backend_client_should_keep_idle_pooled_websocket_alive_across_rep
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let mut request =
@@ -1084,7 +1084,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let mut request =
@@ -1188,7 +1188,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let mut request =
@@ -1292,7 +1292,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(Arc::clone(&pool));
let mut request =
@@ -1387,7 +1387,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}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(pool);
let mut request =
@@ -1467,7 +1467,7 @@ async fn codex_backend_client_should_reuse_pooled_websocket_after_unmapped_respo
let backend = CodexBackendClient::new(
reqwest::Client::builder().no_proxy().build().unwrap(),
format!("http://{addr}"),
crate::support::fingerprint::test_fingerprint(),
crate::support::fingerprint::runtime_test_fingerprint(),
)
.with_websocket_pool(pool);
let request = pooled_websocket_request("conversation-pool-unmapped-failed");