diff --git a/.github/workflows/_quality.yml b/.github/workflows/_quality.yml index 0cd7e22bb..38c9179a1 100644 --- a/.github/workflows/_quality.yml +++ b/.github/workflows/_quality.yml @@ -176,7 +176,7 @@ jobs: cargo nextest run \ --cargo-metadata "$RUNNER_TEMP/nextest-cargo-metadata.json" \ --binaries-metadata "$RUNNER_TEMP/nextest-binaries.json" \ - --no-tests fail + --test-threads 8 --no-tests fail - name: Store tests with Redis ACL authentication working-directory: backend @@ -195,7 +195,7 @@ jobs: cargo nextest run \ --cargo-metadata "$RUNNER_TEMP/nextest-cargo-metadata.json" \ --binaries-metadata "$RUNNER_TEMP/nextest-binaries.json" \ - --no-tests fail -E 'package(=gateway-store) & test(/^redis::/)' + --test-threads 8 --no-tests fail -E 'package(=gateway-store) & test(/^redis::/)' frontend: if: inputs.run_frontend diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 7d4209f59..577dfa0fa 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -49,6 +49,10 @@ opt-level = 2 [profile.dev.package."miniz_oxide@0.9.1"] opt-level = 2 +# workspace 的 WebSocket deflate 特性会让 flate2 选用 zlib-rs +[profile.dev.package.zlib-rs] +opt-level = 2 + [profile.release] debug = false strip = "symbols" diff --git a/backend/crates/gateway-api/tests/admin/accounts/mod.rs b/backend/crates/gateway-api/tests/admin/accounts/mod.rs index dd9672db7..216789597 100644 --- a/backend/crates/gateway-api/tests/admin/accounts/mod.rs +++ b/backend/crates/gateway-api/tests/admin/accounts/mod.rs @@ -851,35 +851,21 @@ mod actions { })) .is_err() ); - assert!( - serde_json::from_value::(json!({ - "provider": "openai", - "data": {}, - "groupIds": [] - })) - .is_err() - ); } #[test] - fn oauth_complete_should_not_accept_group_assignment() { + fn oauth_complete_rejects_unknown_fields() { let flow_id = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([7_u8; 32]); - let request: CompleteAccountAuthorizationRequest = serde_json::from_value(json!({ + let mut payload = json!({ "provider": "openai", "flowId": flow_id, "callbackUrl": "http://localhost/callback?code=ok" - })) - .expect("decode OAuth completion"); + }); + let request: CompleteAccountAuthorizationRequest = + serde_json::from_value(payload.clone()).expect("decode OAuth completion"); assert!(request.validate().is_ok()); - assert!( - serde_json::from_value::(json!({ - "provider": "xai", - "flowId": "flow_test", - "callbackUrl": "http://localhost/callback?code=ok", - "groupIds": [] - })) - .is_err() - ); + payload["unknownField"] = json!(true); + assert!(serde_json::from_value::(payload).is_err()); } #[test] diff --git a/backend/crates/gateway-api/tests/admin/observability/response.rs b/backend/crates/gateway-api/tests/admin/observability/response.rs index bbc1c431f..2d1d2508b 100644 --- a/backend/crates/gateway-api/tests/admin/observability/response.rs +++ b/backend/crates/gateway-api/tests/admin/observability/response.rs @@ -879,7 +879,7 @@ async fn usage_route_should_forward_name_and_model_contains_filters() { } #[tokio::test] -async fn usage_route_should_expose_table_facts_without_detail_payload() { +async fn usage_route_projects_request_facts_and_billing() { use std::str::FromStr as _; use axum::{ @@ -1123,7 +1123,6 @@ async fn usage_route_should_expose_table_facts_without_detail_payload() { "userAgent": "codex-cli/1.0", }) ); - assert!(value["data"]["items"][0].get("metadata").is_none()); assert_eq!( value["data"]["items"][1]["billing"]["totalAmountDisplay"], "≈ $0.007" @@ -1731,7 +1730,7 @@ async fn usage_status_query_accepts_three_modes_and_rejects_unknown_values() { } #[tokio::test] -async fn dashboard_summary_should_identify_metrics_without_localized_labels() { +async fn dashboard_summary_identifies_trend_metrics_and_account_usage_values() { use axum::{ body::{Body, to_bytes}, http::{Request, StatusCode, header}, @@ -1884,7 +1883,6 @@ async fn dashboard_summary_should_identify_metrics_without_localized_labels() { assert_eq!(summary.len(), metrics.len()); for (item, metric) in summary.iter().zip(metrics) { assert_eq!(item["metric"], metric); - assert!(item.get("label").is_none()); assert!(item["value"].is_string()); } } diff --git a/backend/crates/gateway-api/tests/admin/settings.rs b/backend/crates/gateway-api/tests/admin/settings.rs index 859778b4d..382ffdfca 100644 --- a/backend/crates/gateway-api/tests/admin/settings.rs +++ b/backend/crates/gateway-api/tests/admin/settings.rs @@ -426,112 +426,20 @@ fn settings_request_should_reject_non_semver_client_min() { } #[test] -fn settings_response_should_cover_the_full_runtime_settings_contract() { - use std::collections::BTreeMap; - - use chrono::{TimeZone, Utc}; - use gateway_admin::model::Revision; - use gateway_admin::model::settings::RuntimeSettings; +fn settings_response_formats_update_time_in_deployment_timezone() { + use chrono::{TimeZone as _, Utc}; use gateway_api::admin::settings::RuntimeSettingsView; - use gateway_core::account::RotationStrategy; - use gateway_core::routing::{PublicModelId, UpstreamModelId}; - - let settings = RuntimeSettings { - request_profiles: Default::default(), - config_revision: Revision::new(7).expect("revision"), - model_mappings: BTreeMap::from_iter([ - ( - PublicModelId::new("gpt-5.4").expect("public model"), - UpstreamModelId::new("gpt-5.5").expect("upstream model"), - ), - ( - PublicModelId::new("grok-latest").expect("public model"), - UpstreamModelId::new("grok-4.5").expect("upstream model"), - ), - ]), - rotation_strategy: RotationStrategy::RoundRobin, - updated_at: Utc - .with_ymd_and_hms(2026, 8, 2, 10, 30, 0) - .single() - .expect("timestamp"), - values: gateway_admin::model::settings::RuntimeSettingsValues { - codex_privacy_policy: Default::default(), - request_location_enabled: false, - request_location: Default::default(), - refresh_margin_seconds: 1800, - refresh_concurrency: 4, - max_concurrent_per_account: 5, - max_waiting_per_key: 0, - max_waiting_per_account: 0, - concurrency_wait_timeout_seconds: 30, - openai_guardian_reserved_concurrency: 0, - openai_account_affinity: gateway_core::account::AccountAffinity::Relaxed, - max_account_rotations: 3, - openai_session_affinity_ttl_hours: 24, - responses_max_decompressed_body_bytes: 64 * 1024 * 1024, - smart_scheduling: gateway_core::account::SmartSchedulingConfig::default(), - min_codex_desktop_version: Some("26.825.6671".to_owned()), - min_codex_cli_version: Some("0.40.0".to_owned()), - usage_retention_days: 31, - ops_event_retention_days: 31, - audit_retention_days: 91, - account_error_protection: Default::default(), - account_warmup_enabled: false, - account_warmup_schedule_time: "08:00".to_owned(), - account_warmup_model: None, - reset_credit_lead_minutes: 10, - reset_credit_max_weekly_remaining_percent: None, - }, - }; + let mut settings = super::test_runtime_settings(); + settings.updated_at = Utc.with_ymd_and_hms(2026, 8, 2, 10, 30, 0).unwrap(); let value = serde_json::to_value(RuntimeSettingsView::from(( settings, gateway_api::TimePresenter::new(Default::default()), ))) .expect("serialize view"); - assert_eq!( - value, - json!({ - "configRevision": 7, - "providerRequestProfiles": {}, - "openaiClientProfile": null, - "xaiClientProfile": null, - "codexPrivacyPolicy": {"enabled":false,"onError":"skip_rule","rules":[]}, - "requestLocationEnabled": false, - "requestLocation": {"country":"US", "region":"Ohio", "city":"Piketon", "timezone":"America/New_York"}, - "modelMappings": { - "gpt-5.4": "gpt-5.5", - "grok-latest": "grok-4.5" - }, - "refreshMarginSeconds": 1800, - "refreshConcurrency": 4, - "maxConcurrentPerAccount": 5, - "maxWaitingPerKey": 0, - "maxWaitingPerAccount": 0, - "openaiGuardianReservedConcurrency": 0, - "openaiAccountAffinity": "relaxed", - "maxAccountRotations": 3, - "openaiSessionAffinityTtlHours": 24, - "concurrencyWaitTimeoutSeconds": 30, - "responsesMaxDecompressedBodyBytes": 67108864, - "rotationStrategy": "round_robin", - "smartScheduling": gateway_core::account::SmartSchedulingConfig::default(), - "smartSchedulingDefaults": gateway_core::account::SmartSchedulingConfig::default(), - "minCodexDesktopVersion": "26.825.6671", - "minCodexCliVersion": "0.40.0", - "usageRetentionDays": 31, - "opsEventRetentionDays": 31, - "auditRetentionDays": 91, - "accountErrorProtection": gateway_core::account::AccountErrorProtection::default(), - "accountWarmupEnabled": false, - "accountWarmupScheduleTime": "08:00", - "accountWarmupModel": null, - "resetCreditLeadMinutes": 10, - "resetCreditMaxWeeklyRemainingPercent": null, - "updatedAt": "2026-08-02T10:30:00Z", - "updatedAtDisplay": "2026-08-02 18:30:00" - }) - ); + + assert_eq!(value["updatedAt"], "2026-08-02T10:30:00Z"); + assert_eq!(value["updatedAtDisplay"], "2026-08-02 18:30:00"); } #[test] diff --git a/backend/crates/gateway-host/tests/config.rs b/backend/crates/gateway-host/tests/config.rs index 83b2a2a8c..8c48c14ba 100644 --- a/backend/crates/gateway-host/tests/config.rs +++ b/backend/crates/gateway-host/tests/config.rs @@ -53,7 +53,7 @@ fn host_config_should_reject_empty_update_asset_directory() { } #[test] -fn host_config_resolves_only_host_owned_relative_paths() { +fn host_config_resolves_resource_paths_relative_to_configuration() { let mut config = valid_config(); config .resolve_and_validate( @@ -66,34 +66,10 @@ fn host_config_resolves_only_host_owned_relative_paths() { config.logging.file.directory, PathBuf::from("/srv/gateway/.runtime/logs") ); -} - -#[test] -fn host_config_resolves_runtime_data_dir_relative_to_configuration() { - let mut config = valid_config(); - config - .resolve_and_validate( - std::path::Path::new("/srv/gateway"), - std::path::Path::new("/srv/gateway/web/dist"), - ) - .expect("valid host config"); - assert_eq!( config.runtime_data_dir(), PathBuf::from("/srv/gateway/runtime-data") ); -} - -#[test] -fn host_config_derives_update_paths_from_runtime_data_dir() { - let mut config = valid_config(); - config - .resolve_and_validate( - std::path::Path::new("/srv/gateway"), - std::path::Path::new("/srv/gateway/web/dist"), - ) - .expect("valid host config"); - assert_eq!( ( config.system_update.update_state_file, @@ -124,23 +100,19 @@ fn host_config_rejects_zero_worker_shutdown_window() { } #[test] -fn logging_rejects_zero_retention_windows() { - for request_dump in [false, true] { - let mut config = valid_config(); - if request_dump { - config.logging.request_dump_retention_days = 0.0; - } else { - config.logging.file.retention_days = 0; - } - assert!( - config - .resolve_and_validate( - std::path::Path::new("/srv/gateway"), - std::path::Path::new("/srv/gateway/web/dist"), - ) - .is_err() - ); - } +fn file_logging_rejects_zero_retention() { + let mut config = valid_config(); + config.logging.file.retention_days = 0; + let error = config + .resolve_and_validate( + std::path::Path::new("/srv/gateway"), + std::path::Path::new("/srv/gateway/web/dist"), + ) + .unwrap_err(); + assert!(matches!( + error, + gateway_host::ConfigError::InvalidField("host.logging.file.retention_days") + )); } #[test] diff --git a/backend/crates/gateway-plugin/sdk/tests/manifest.rs b/backend/crates/gateway-plugin/sdk/tests/manifest.rs index b51561bc3..fe763a6c1 100644 --- a/backend/crates/gateway-plugin/sdk/tests/manifest.rs +++ b/backend/crates/gateway-plugin/sdk/tests/manifest.rs @@ -282,14 +282,22 @@ fn icon_wire_is_a_path_or_exact_light_dark_object() { #[test] fn contribution_ids_are_owned_unique_and_stage_bounded() { let mut manifest = source_manifest(); - let middleware = manifest - .contributes - .get(&Capability::Middleware) - .unwrap() - .clone(); + manifest.contributes.insert( + Capability::Scheduler, + ContributionDeclaration { + id: "9acme.request-tags.scheduler".into(), + version: 3, + stages: vec![Stage::Scheduling], + input_formats: Vec::new(), + output_formats: Vec::new(), + }, + ); + assert!(manifest.validate().is_ok()); manifest .contributes - .insert(Capability::Scheduler, middleware); + .get_mut(&Capability::Scheduler) + .unwrap() + .id = manifest.contributes[&Capability::Middleware].id.clone(); assert_eq!(manifest.validate(), Err(ManifestError::Invalid)); let mut manifest = source_manifest(); @@ -316,13 +324,19 @@ fn installed_manifest_rejects_non_middleware_stage_drift() { Capability::Management, ContributionDeclaration { id: "9acme.request-tags.management".into(), - version: 1, - stages: vec![Stage::Registration], + version: 2, + stages: vec![Stage::Management], input_formats: Vec::new(), output_formats: Vec::new(), }, ); + assert!(manifest.validate().is_ok()); + manifest + .contributes + .get_mut(&Capability::Management) + .unwrap() + .stages = vec![Stage::Registration]; assert_eq!(manifest.validate(), Err(ManifestError::Invalid)); } diff --git a/backend/crates/gateway-store/tests/postgres/backup.rs b/backend/crates/gateway-store/tests/postgres/backup.rs index ac65520da..17b9b8fc6 100644 --- a/backend/crates/gateway-store/tests/postgres/backup.rs +++ b/backend/crates/gateway-store/tests/postgres/backup.rs @@ -235,8 +235,10 @@ async fn backup_lifecycle_transitions_through_deleting_and_hard_delete() { assert_eq!(record.status, BackupStatus::Queued); assert_eq!(record.attempt_count, 0); + // 生命周期时间统一从数据库创建时间推导,避免测试进程与数据库的时钟偏差 + let started_at = record.created_at + chrono::Duration::seconds(1); let claimed = repository - .claim_next_queued(Utc::now()) + .claim_next_queued(started_at) .await .expect("claim") .expect("claimed one task"); @@ -254,7 +256,7 @@ async fn backup_lifecycle_transitions_through_deleting_and_hard_delete() { &record.id, transition(BackupStatus::Dumping, BackupStatus::Uploading), update, - Utc::now(), + started_at + chrono::Duration::seconds(1), ) .await .expect("dump complete") @@ -267,7 +269,7 @@ async fn backup_lifecycle_transitions_through_deleting_and_hard_delete() { &record.id, transition(BackupStatus::Uploading, BackupStatus::Completed), StatusTransitionUpdate::default(), - Utc::now(), + started_at + chrono::Duration::seconds(2), ) .await .expect("upload verified") @@ -276,7 +278,11 @@ async fn backup_lifecycle_transitions_through_deleting_and_hard_delete() { assert!(completed.completed_at.is_some()); let deleting = repository - .transition_to_deleting(&record.id, Utc::now(), Some(&context())) + .transition_to_deleting( + &record.id, + started_at + chrono::Duration::seconds(3), + Some(&context()), + ) .await .expect("to deleting") .expect("transition matched"); @@ -304,7 +310,7 @@ async fn active_task_unique_index_blocks_second_queued() { }; let repository = PgBackupRepository::new(db.pool.clone()); - repository + let record = repository .insert_backup_record( seed(&"a".repeat(32), BackupTriggerKind::Manual, None), &context(), @@ -321,8 +327,10 @@ async fn active_task_unique_index_blocks_second_queued() { assert_eq!(error.kind(), AdminStoreErrorKind::Conflict); // 第一个任务完成释放名额后,第二个可以插入 + let started_at = record.created_at + chrono::Duration::seconds(1); + let completed_at = started_at + chrono::Duration::seconds(1); let claimed = repository - .claim_next_queued(Utc::now()) + .claim_next_queued(started_at) .await .expect("claim") .expect("claimed"); @@ -333,10 +341,10 @@ async fn active_task_unique_index_blocks_second_queued() { StatusTransitionUpdate { error_code: Some("backup.pg_dump_failed".to_owned()), error_message: Some("boom".to_owned()), - completed_at: Some(Utc::now()), + completed_at: Some(completed_at), ..Default::default() }, - Utc::now(), + completed_at, ) .await .expect("fail task"); @@ -605,12 +613,13 @@ async fn manual_backup_mutations_roll_back_when_their_audit_cannot_be_written() .execute(&db.pool) .await .expect("restore audit writes"); - repository + let record = repository .insert_backup_record(seed, &context()) .await .expect("create with audit"); + let started_at = record.created_at + chrono::Duration::seconds(1); repository - .claim_next_queued(Utc::now()) + .claim_next_queued(started_at) .await .expect("claim backup fixture") .expect("queued backup is claimed"); @@ -623,7 +632,7 @@ async fn manual_backup_mutations_roll_back_when_their_audit_cannot_be_written() error_message: Some("synthetic fixture failure".to_owned()), ..Default::default() }, - Utc::now(), + started_at + chrono::Duration::seconds(1), ) .await .expect("finish backup fixture") @@ -632,7 +641,11 @@ async fn manual_backup_mutations_roll_back_when_their_audit_cannot_be_written() .execute(&db.pool).await.expect("reject deletion audit"); assert!( repository - .transition_to_deleting(&backup_id, Utc::now(), Some(&context())) + .transition_to_deleting( + &backup_id, + started_at + chrono::Duration::seconds(2), + Some(&context()), + ) .await .is_err() ); @@ -651,14 +664,22 @@ async fn manual_backup_mutations_roll_back_when_their_audit_cannot_be_written() .expect("restore deletion audit"); assert!( repository - .transition_to_deleting(&backup_id, Utc::now(), Some(&context())) + .transition_to_deleting( + &backup_id, + started_at + chrono::Duration::seconds(3), + Some(&context()), + ) .await .unwrap() .is_some() ); assert!( repository - .transition_to_deleting(&backup_id, Utc::now(), Some(&context())) + .transition_to_deleting( + &backup_id, + started_at + chrono::Duration::seconds(4), + Some(&context()), + ) .await .unwrap() .is_none() diff --git a/backend/crates/gateway-store/tests/postgres/provider_accounts/admin_adapter.rs b/backend/crates/gateway-store/tests/postgres/provider_accounts/admin_adapter.rs index f5fc8e8d4..cee915a68 100644 --- a/backend/crates/gateway-store/tests/postgres/provider_accounts/admin_adapter.rs +++ b/backend/crates/gateway-store/tests/postgres/provider_accounts/admin_adapter.rs @@ -614,7 +614,10 @@ async fn reset_credit_automation_persists_pending_checks_cas_and_follows_account .await .unwrap(); let store = admin_account_store(&database.pool); - let now = Utc::now(); + let now: chrono::DateTime = sqlx::query_scalar("select now()") + .fetch_one(&database.pool) + .await + .unwrap(); let mut record = ResetCreditAutomation { account_id: "acct_automation".into(), enabled: true, @@ -676,6 +679,11 @@ async fn reset_credit_automation_persists_pending_checks_cas_and_follows_account ); update.values.reset_credit_lead_minutes = 20; settings.update_runtime_settings(update).await.unwrap(); + // 重规划使用数据库时钟,立即到期的断言与后续查询保持相同时间源 + let replanned_at: chrono::DateTime = sqlx::query_scalar("select now()") + .fetch_one(&database.pool) + .await + .unwrap(); let replanned = store .reset_credit_automation(&record.account_id) .await @@ -686,12 +694,12 @@ async fn reset_credit_automation_persists_pending_checks_cas_and_follows_account "replan preserves pending idempotency key" ); assert!(replanned.revision > record.revision); - assert!(replanned.next_run_at <= Utc::now()); + assert!(replanned.next_run_at <= replanned_at); record = replanned; assert_eq!( store - .due_reset_credit_automations(now + TimeDelta::seconds(1), 1) + .due_reset_credit_automations(replanned_at, 1) .await .unwrap(), [record.account_id.clone()] @@ -715,7 +723,7 @@ async fn reset_credit_automation_persists_pending_checks_cas_and_follows_account ); assert!( store - .due_reset_credit_automations(now + TimeDelta::seconds(1), 10) + .due_reset_credit_automations(replanned_at, 10) .await .unwrap() .is_empty() @@ -740,7 +748,7 @@ async fn reset_credit_automation_persists_pending_checks_cas_and_follows_account ); assert!( store - .due_reset_credit_automations(now + TimeDelta::seconds(1), 10) + .due_reset_credit_automations(replanned_at, 10) .await .unwrap() .is_empty() diff --git a/backend/crates/gateway-store/tests/redis/client_admission.rs b/backend/crates/gateway-store/tests/redis/client_admission.rs index e68cef516..0dd6628db 100644 --- a/backend/crates/gateway-store/tests/redis/client_admission.rs +++ b/backend/crates/gateway-store/tests/redis/client_admission.rs @@ -96,7 +96,8 @@ async fn explicit_deadline_releases_capacity_without_becoming_cancellation() { let Some((repository, mut connection, namespace)) = repository().await else { return; }; - let first = admission_request("req_timed", "key-timed", Duration::from_secs(1)); + // 初始租约长于等待上限,确保容量由显式截止释放,而非原始 TTL 自然到期 + let first = admission_request("req_timed", "key-timed", Duration::from_secs(30)); assert_eq!( repository.admit_client_request(&first).await.unwrap(), ClientAdmissionDecision::Granted @@ -108,14 +109,22 @@ async fn explicit_deadline_releases_capacity_without_becoming_cancellation() { (std::time::SystemTime::now() + Duration::from_millis(300)).into(), cancellation.clone(), ); - tokio::time::sleep(Duration::from_millis(500)).await; - assert!(!cancellation.is_cancelled()); let mut next = admission_request("req_next", "key-timed", Duration::from_secs(1)); next.limits.max_concurrency = 1; - assert_eq!( - repository.admit_client_request(&next).await.unwrap(), - ClientAdmissionDecision::Granted - ); + tokio::time::timeout(Duration::from_secs(2), async { + loop { + match repository.admit_client_request(&next).await.unwrap() { + ClientAdmissionDecision::Granted => break, + ClientAdmissionDecision::Rejected(ClientAdmissionRejection::ConcurrencyLimited) => { + tokio::time::sleep(Duration::from_millis(10)).await; + } + decision => panic!("unexpected admission decision: {decision:?}"), + } + } + }) + .await + .expect("explicit deadline releases admission capacity"); + assert!(!cancellation.is_cancelled()); drop(renewal); delete_namespace_keys(&mut connection, &namespace).await; } diff --git a/backend/crates/gateway-store/tests/redis/credential_cooldown.rs b/backend/crates/gateway-store/tests/redis/credential_cooldown.rs index 95bf8f77f..eee936009 100644 --- a/backend/crates/gateway-store/tests/redis/credential_cooldown.rs +++ b/backend/crates/gateway-store/tests/redis/credential_cooldown.rs @@ -1,6 +1,6 @@ //! 验证账号冷却缓存的版本隔离、期限延长与到期清理 -use std::time::{Duration as StdDuration, SystemTime}; +use std::time::SystemTime; use chrono::{DateTime, Duration, Utc}; use gateway_core::{ @@ -159,18 +159,51 @@ async fn credential_cooldown_read_removes_expired_grace_key() { let Some((repository, mut connection, namespace)) = repository().await else { return; }; - let cooldown_until = Utc::now() + Duration::milliseconds(40); + let (seconds, microseconds): (i64, i64) = redis::cmd("TIME") + .query_async(&mut connection) + .await + .expect("read Redis time"); + let now_ms = seconds * 1_000 + microseconds / 1_000; let cooldown = CredentialCooldown { provider_account_id: "acct_cooldown_expiry".to_owned(), credential_revision: Revision::new(1).expect("positive revision"), - cooldown_until: millisecond_precision(cooldown_until), + cooldown_until: DateTime::from_timestamp_millis(now_ms + 30_000).unwrap(), kind: ProviderCooldownKind::RateLimit, }; - repository - .cache_credential_cooldown(&cooldown) + assert!( + repository + .cache_credential_cooldown(&cooldown) + .await + .expect("cache cooldown") + ); + let keys = namespace_keys(&mut connection, &namespace).await; + let cooldown_key = keys + .iter() + .find(|key| key.ends_with(":cooldown")) + .expect("cooldown grace key"); + let index_key = keys + .iter() + .find(|key| key.ends_with(":account:active-cooldowns")) + .expect("active cooldown index"); + // 把逻辑期限设为已过期并保留实体 TTL,确保由读取路径清理 grace key + let (new_fields, new_members, ttl): (i64, i64, i64) = redis::pipe() + .atomic() + .cmd("HSET") + .arg(cooldown_key) + .arg("until_ms") + .arg(now_ms - 1) + .cmd("ZADD") + .arg(index_key) + .arg(now_ms - 1) + .arg(&cooldown.provider_account_id) + .cmd("PTTL") + .arg(cooldown_key) + .query_async(&mut connection) .await - .expect("cache short cooldown"); - tokio::time::sleep(StdDuration::from_millis(80)).await; + .expect("expire existing cooldown within its grace TTL"); + assert_eq!(new_fields, 0, "expiry field already exists"); + assert_eq!(new_members, 0, "account is already indexed"); + assert!(ttl > 0, "expired cooldown still has a grace key"); assert_eq!( repository diff --git a/backend/crates/providers/openai/tests/transport/catalog.rs b/backend/crates/providers/openai/tests/transport/catalog.rs index ea4ea1f09..178891e7a 100644 --- a/backend/crates/providers/openai/tests/transport/catalog.rs +++ b/backend/crates/providers/openai/tests/transport/catalog.rs @@ -129,12 +129,11 @@ fn service_tiers_should_preserve_declared_ids_names_and_descriptions() { } #[test] -fn missing_or_empty_service_tiers_should_not_infer_fast_from_legacy_metadata() { +fn missing_or_empty_service_tiers_should_remain_empty() { for service_tiers in [None, Some(json!([]))] { let mut model = json!({ "slug": "gpt-5.4", - "display_name": "GPT-5.4", - "additional_speed_tiers": ["fast"] + "display_name": "GPT-5.4" }); if let Some(service_tiers) = service_tiers { model["service_tiers"] = service_tiers; diff --git a/backend/crates/providers/openai/tests/transport/profile/desktop_artifact.rs b/backend/crates/providers/openai/tests/transport/profile/desktop_artifact.rs index dc3de473a..bd8fd15f1 100644 --- a/backend/crates/providers/openai/tests/transport/profile/desktop_artifact.rs +++ b/backend/crates/providers/openai/tests/transport/profile/desktop_artifact.rs @@ -90,17 +90,12 @@ fn central_directory_rejects_duplicate_core_entries() { } #[test] -fn central_directory_rejects_legacy_core_and_cli_launcher_paths() { - for name in [ - b"ChatGPT.app/Contents/Resources/codex".as_slice(), - b"ChatGPT.app/Contents/Resources/codex-cli/bin/codex", - ] { - let central = deflated_entry(name); - assert!(matches!( - find_core_entry(¢ral, 1), - Err(CodexDesktopArtifactError::MissingCore) - )); - } +fn central_directory_with_only_cli_launcher_is_missing_core() { + let central = deflated_entry(b"ChatGPT.app/Contents/Resources/codex-cli/bin/codex"); + assert!(matches!( + find_core_entry(¢ral, 1), + Err(CodexDesktopArtifactError::MissingCore) + )); } fn deflated_entry(name: &[u8]) -> Vec { diff --git a/backend/crates/providers/openai/tests/transport/proxy_capacity.rs b/backend/crates/providers/openai/tests/transport/proxy_capacity.rs index a75cf6433..4d8224e69 100644 --- a/backend/crates/providers/openai/tests/transport/proxy_capacity.rs +++ b/backend/crates/providers/openai/tests/transport/proxy_capacity.rs @@ -695,6 +695,8 @@ impl LimitedProxy { tokio::select! { accepted = listener.accept() => { let (mut stream, _) = accepted.unwrap(); + // 容量测试不测 TCP 合包,立即发送短帧避免每次握手等待延迟 ACK + stream.set_nodelay(true).unwrap(); let (slots, active, peak, opened) = (slots.clone(), a.clone(), p.clone(), o.clone()); let (connect_gate, response_gate, requests) = (connect_gate.clone(), response_gate.clone(), recorded.clone()); let reject_connects = rejects.clone(); diff --git a/backend/crates/providers/openai/tests/transport/websocket_pool.rs b/backend/crates/providers/openai/tests/transport/websocket_pool.rs index 0598771d0..782c589b5 100644 --- a/backend/crates/providers/openai/tests/transport/websocket_pool.rs +++ b/backend/crates/providers/openai/tests/transport/websocket_pool.rs @@ -1123,9 +1123,9 @@ async fn concurrent_pool_transport_counts(max_connecting: usize) -> (usize, usiz let addr = listener.local_addr().unwrap(); let server = tokio::spawn(async move { let mut websocket_streams = Vec::new(); - let mut http_count = 0; + let mut http_streams = Vec::new(); for _ in 0..REQUEST_COUNT { - let (mut stream, _) = listener.accept().await.unwrap(); + let (stream, _) = listener.accept().await.unwrap(); let method = timeout(Duration::from_secs(2), async { let mut prefix = [0_u8; 4]; loop { @@ -1141,13 +1141,17 @@ async fn concurrent_pool_transport_counts(max_connecting: usize) -> (usize, usiz if method == *b"GET " { websocket_streams.push(stream); } else { - let request = read_http_request(&mut stream).await; - assert!(request.starts_with("POST /codex/responses HTTP/1.1")); - write_completed_sse_response(&mut stream).await; - http_count += 1; + http_streams.push(stream); } } + // 收齐建连尝试后再响应,避免 HTTP 连接复用或重试干扰 opening 名额断言 + let http_count = http_streams.len(); + for mut stream in http_streams { + let request = read_http_request_with_body(&mut stream).await; + assert!(request.starts_with(b"POST /codex/responses HTTP/1.1")); + write_completed_sse_response(&mut stream).await; + } let websocket_count = websocket_streams.len(); for (index, stream) in websocket_streams.into_iter().enumerate() { let mut websocket = accept_codex_test_websocket(stream).await; diff --git a/backend/crates/providers/xai/tests/transport/catalog.rs b/backend/crates/providers/xai/tests/transport/catalog.rs index 1e80fcb42..adb1db38e 100644 --- a/backend/crates/providers/xai/tests/transport/catalog.rs +++ b/backend/crates/providers/xai/tests/transport/catalog.rs @@ -425,136 +425,69 @@ fn missing_capability_fields_should_remain_unknown() { } #[test] -fn reasoning_effort_menu_should_be_capability_evidence_without_legacy_flag() { - let snapshot = parse_grok_model_catalog( - br#"{"object":"list","data":[{"id":"grok-reasoning","reasoning_efforts":[{"value":"xhigh","default":true},{"value":"low"}],"model":"grok-reasoning"}]}"#, - None, - ) - .expect("reasoning effort menu"); - let capabilities = snapshot.models()[0].capabilities(); - - assert_eq!( +fn reasoning_menu_preserves_known_options_and_explicit_defaults() { + for (menu, expected_efforts, expected_default, expected_evidence) in [ ( - capabilities.reasoning_effort(), - capabilities - .reasoning_efforts() - .iter() - .map(|effort| effort.as_str()) - .collect::>(), - capabilities - .default_reasoning_effort() - .map(|effort| effort.as_str()), - ), - ( - GrokCatalogCapabilityEvidence::DeclaredNative, + serde_json::json!([{"value":"xhigh","default":true},{"value":"low"}]), vec!["xhigh", "low"], Some("xhigh"), - ) - ); -} - -#[test] -fn historical_and_other_endpoint_fields_do_not_supply_model_facts() { - let snapshot = parse_grok_model_catalog( - br#"{"object":"list","data":[{"model":"grok-future","contextWindow":500000,"contextWindows":[1000000],"apiBackend":"responses","reasoningEfforts":[{"value":"high","default":true}],"features":{"reasoning":true,"reasoningEffortOptions":{"supportedEfforts":["high"],"defaultEffort":"high"}},"_meta":{"contextWindow":500000,"reasoningEfforts":[{"value":"high","default":true}]},"capabilities":{"reasoning_effort":["high"],"default_reasoning_effort":"high"},"reasoning_effort":"high"}]}"#, - None, - ).expect("unknown fields do not define CLI proxy facts"); - let model = &snapshot.models()[0]; - assert_eq!( - model.capabilities().reasoning_effort(), - GrokCatalogCapabilityEvidence::Unknown - ); - assert!(model.capabilities().reasoning_efforts().is_empty()); - assert_eq!(model.capabilities().default_reasoning_effort(), None); - assert_eq!(model.limits().context_window_tokens(), None); - assert_eq!(model.limits().max_context_window_tokens(), None); - assert_eq!( - model.capabilities().responses_api(), - GrokCatalogCapabilityEvidence::Unknown - ); -} - -#[test] -fn unknown_reasoning_effort_values_should_not_fail_the_snapshot() { - let snapshot = parse_grok_model_catalog( - br#"{"object":"list","data":[{"id":"grok-reasoning","reasoning_effort":"ultra","reasoning_efforts":[{"value":"ultra","default":true},{"value":"low"},{"value":"hyper"}],"model":"grok-reasoning"}]}"#, - None, - ) - .expect("unknown effort values must not break catalog parsing"); - let capabilities = snapshot.models()[0].capabilities(); - - assert_eq!( - ( - capabilities - .reasoning_efforts() - .iter() - .map(|effort| effort.as_str()) - .collect::>(), - capabilities - .default_reasoning_effort() - .map(|effort| effort.as_str()), + GrokCatalogCapabilityEvidence::DeclaredNative, ), - (vec!["low"], None) - ); -} - -#[test] -fn reasoning_menu_uses_only_known_explicit_default() { - for (menu, expected) in [ ( serde_json::json!([{"value":"low"},{"value":"high","default":true}]), + vec!["low", "high"], Some("high"), + GrokCatalogCapabilityEvidence::DeclaredNative, ), - (serde_json::json!([{"value":"low"},{"value":"high"}]), None), ( - serde_json::json!([{"value":"low"},{"value":"quantum","default":true}]), + serde_json::json!([{"value":"low"},{"value":"high"}]), + vec!["low", "high"], None, + GrokCatalogCapabilityEvidence::DeclaredNative, + ), + ( + serde_json::json!([{"value":"ultra","default":true},{"value":"low"},{"value":"hyper"}]), + vec!["low"], + None, + GrokCatalogCapabilityEvidence::DeclaredNative, + ), + ( + serde_json::json!([{"value":"quantum","default":true}]), + vec![], + None, + GrokCatalogCapabilityEvidence::Unknown, + ), + ( + serde_json::json!([]), + vec![], + None, + GrokCatalogCapabilityEvidence::Unknown, ), ] { let body = serde_json::to_vec(&serde_json::json!({ "object":"list", "data":[{"model":"grok-4.7","reasoning_efforts":menu}] })) .expect("fixture"); - let snapshot = parse_grok_model_catalog(&body, None).expect("current menu"); + let snapshot = parse_grok_model_catalog(&body, None).expect("reasoning menu"); + let capabilities = snapshot.models()[0].capabilities(); assert_eq!( - snapshot.models()[0] - .capabilities() - .default_reasoning_effort() - .map(|effort| effort.as_str()), - expected + ( + capabilities + .reasoning_efforts() + .iter() + .map(|effort| effort.as_str()) + .collect::>(), + capabilities + .default_reasoning_effort() + .map(|effort| effort.as_str()), + capabilities.reasoning_effort(), + ), + (expected_efforts, expected_default, expected_evidence), + "{menu}", ); } } -#[test] -fn current_reasoning_menu_does_not_borrow_defaults_from_other_sources() { - let snapshot = parse_grok_model_catalog( - br#"{"object":"list","data":[{"model":"grok-future","reasoning_efforts":[{"value":"low"}],"reasoning_effort":"low","capabilities":{"reasoning_effort":["high"],"default_reasoning_effort":"high"}}]}"#, - None, - ).expect("current menu"); - let capabilities = snapshot.models()[0].capabilities(); - assert_eq!( - capabilities - .reasoning_efforts() - .iter() - .map(|effort| effort.as_str()) - .collect::>(), - ["low"] - ); - assert_eq!(capabilities.default_reasoning_effort(), None); -} - -#[test] -fn unknown_reasoning_menu_does_not_fall_back_to_other_sources() { - let snapshot = parse_grok_model_catalog( - br#"{"object":"list","data":[{"model":"grok-future","reasoning_efforts":[{"value":"quantum","default":true}],"capabilities":{"reasoning_effort":["high"],"default_reasoning_effort":"high"}}]}"#, - None, - ).expect("unknown effort"); - let capabilities = snapshot.models()[0].capabilities(); - assert!(capabilities.reasoning_efforts().is_empty()); - assert_eq!(capabilities.default_reasoning_effort(), None); -} - #[test] fn context_window_choices_supply_default_when_scalar_is_missing() { let snapshot = parse_grok_model_catalog( @@ -614,10 +547,7 @@ fn malformed_context_window_choices_are_rejected() { #[test] fn current_proxy_requires_the_explicit_request_model() { - for entry in [ - serde_json::json!({"id":"grok-4.7"}), - serde_json::json!({"modelId":"grok-4.7"}), - ] { + for entry in [serde_json::json!({"id":"grok-4.7"}), serde_json::json!({})] { let body = serde_json::to_vec(&serde_json::json!({"object":"list","data":[entry]})) .expect("fixture"); assert!(matches!( diff --git a/backend/crates/providers/xai/tests/transport/request/web_search.rs b/backend/crates/providers/xai/tests/transport/request/web_search.rs index a050a4117..2f4674586 100644 --- a/backend/crates/providers/xai/tests/transport/request/web_search.rs +++ b/backend/crates/providers/xai/tests/transport/request/web_search.rs @@ -71,7 +71,7 @@ fn web_search_filters_keep_empty_lists_unbounded() { } #[test] -fn web_search_filters_reject_retired_top_level_allowed_domains() { +fn web_search_filters_reject_top_level_allowed_domains() { for tool in [ json!({"type": "web_search_preview", "allowed_domains": ["example.com"]}), json!({"type": "web_search", "allowed_domains": ["example.com"], diff --git a/docs/architecture.md b/docs/architecture.md index bf0a114b1..7cd29a4f9 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -1282,7 +1282,8 @@ RUST_MIN_STACK=16777216 cargo +1.99.1 nextest run --manifest-path backend/Cargo. ``` 测试使用 [nextest](https://nexte.st/docs/installation/pre-built-binaries/),版本与[质量工作流](../.github/workflows/_quality.yml)一致 -各 crate 的 `main` 测试入口统一构建,用例按可用 CPU 数跨 crate 并发执行 +各 crate 的 `main` 测试入口统一构建,用例跨 crate 并发执行;本地默认按可用 CPU 数调度, +CI 使用 `--test-threads 8` 并发推进数据库、子进程和网络等待 局部验证可追加 `-E 'package(=gateway-store) & test(/^redis::/)'` 等过滤条件;资源受限时用 `--test-threads 4` 限制同时运行的用例数,编译并发另由 `--build-jobs` 控制 未安装 nextest 时可使用 `cargo +1.99.1 test --manifest-path backend/Cargo.toml --test main --locked`,