mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 00:06:08 +00:00
test: streamline backend tests and speed up validation
Remove obsolete and redundant assertions while preserving current behavior. Optimize zlib-rs in dev/test builds and use eight concurrent CI test workers. Use explicit time bases and synchronization for integration fixtures. Validated 4322 tests, 72 Redis ACL tests, and ten stress iterations of 102 concurrency-related tests. Rustfmt, strict Clippy, and actionlint passed. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -851,35 +851,21 @@ mod actions {
|
||||
}))
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
serde_json::from_value::<AccountImportRequest>(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::<CompleteAccountAuthorizationRequest>(json!({
|
||||
"provider": "xai",
|
||||
"flowId": "flow_test",
|
||||
"callbackUrl": "http://localhost/callback?code=ok",
|
||||
"groupIds": []
|
||||
}))
|
||||
.is_err()
|
||||
);
|
||||
payload["unknownField"] = json!(true);
|
||||
assert!(serde_json::from_value::<CompleteAccountAuthorizationRequest>(payload).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<Utc> = 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<Utc> = 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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<u8> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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::<Vec<_>>(),
|
||||
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::<Vec<_>>(),
|
||||
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::<Vec<_>>(),
|
||||
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::<Vec<_>>(),
|
||||
["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!(
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user