mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-05 00:47:48 +08:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
@@ -389,6 +389,9 @@ async fn gateway_executes_openai_responses_compact_openai_family_upstream_stream
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let response_json: serde_json::Value = response.json().await.expect("body should parse");
|
||||
let created_at = response_json["created_at"]
|
||||
.as_i64()
|
||||
.expect("created_at should be a unix timestamp");
|
||||
assert_eq!(
|
||||
response_json,
|
||||
json!({
|
||||
@@ -396,6 +399,9 @@ async fn gateway_executes_openai_responses_compact_openai_family_upstream_stream
|
||||
"object": "response",
|
||||
"model": "gpt-5",
|
||||
"status": "completed",
|
||||
"created_at": created_at,
|
||||
"completed_at": created_at,
|
||||
"output_text": "Hello Compact",
|
||||
"output": [{
|
||||
"type": "message",
|
||||
"id": "resp_compact_openai_family_123_msg",
|
||||
|
||||
@@ -389,6 +389,9 @@ async fn gateway_executes_openai_responses_cross_format_upstream_stream_via_loca
|
||||
assert_eq!(response_status, StatusCode::OK);
|
||||
let response_json: serde_json::Value =
|
||||
serde_json::from_str(&response_body).expect("body should parse");
|
||||
let created_at = response_json["created_at"]
|
||||
.as_i64()
|
||||
.expect("created_at should be a unix timestamp");
|
||||
assert_eq!(
|
||||
response_json,
|
||||
json!({
|
||||
@@ -396,6 +399,9 @@ async fn gateway_executes_openai_responses_cross_format_upstream_stream_via_loca
|
||||
"object": "response",
|
||||
"status": "completed",
|
||||
"model": "gemini-2.5-pro-upstream",
|
||||
"created_at": created_at,
|
||||
"completed_at": created_at,
|
||||
"output_text": "Hello Gemini CLI",
|
||||
"output": [{
|
||||
"type": "message",
|
||||
"id": "upstream-cli-stream-123_msg",
|
||||
@@ -842,6 +848,9 @@ async fn gateway_executes_openai_responses_cross_format_function_call_upstream_s
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let response_json: serde_json::Value = response.json().await.expect("body should parse");
|
||||
let created_at = response_json["created_at"]
|
||||
.as_i64()
|
||||
.expect("created_at should be a unix timestamp");
|
||||
assert_eq!(
|
||||
response_json,
|
||||
json!({
|
||||
@@ -849,6 +858,9 @@ async fn gateway_executes_openai_responses_cross_format_function_call_upstream_s
|
||||
"object": "response",
|
||||
"status": "completed",
|
||||
"model": "gemini-2.5-pro-upstream",
|
||||
"created_at": created_at,
|
||||
"completed_at": created_at,
|
||||
"output_text": "Need a tool.",
|
||||
"output": [
|
||||
{
|
||||
"type": "message",
|
||||
@@ -1412,6 +1424,9 @@ async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_str
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let response_json: serde_json::Value = response.json().await.expect("body should parse");
|
||||
let created_at = response_json["created_at"]
|
||||
.as_i64()
|
||||
.expect("created_at should be a unix timestamp");
|
||||
assert_eq!(
|
||||
response_json,
|
||||
json!({
|
||||
@@ -1419,6 +1434,9 @@ async fn gateway_executes_openai_responses_antigravity_cross_format_upstream_str
|
||||
"object": "response",
|
||||
"status": "completed",
|
||||
"model": "claude-sonnet-4-5",
|
||||
"created_at": created_at,
|
||||
"completed_at": created_at,
|
||||
"output_text": "Hello Antigravity",
|
||||
"output": [{
|
||||
"type": "message",
|
||||
"id": "resp-local-stream_msg",
|
||||
|
||||
@@ -405,6 +405,9 @@ async fn gateway_executes_openai_responses_sync_upstream_stream_via_local_finali
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let response_json: serde_json::Value = response.json().await.expect("body should parse");
|
||||
let created_at = response_json["created_at"]
|
||||
.as_i64()
|
||||
.expect("created_at should be a unix timestamp");
|
||||
assert_eq!(
|
||||
response_json,
|
||||
json!({
|
||||
@@ -412,6 +415,9 @@ async fn gateway_executes_openai_responses_sync_upstream_stream_via_local_finali
|
||||
"object": "response",
|
||||
"model": "gpt-5-upstream",
|
||||
"status": "completed",
|
||||
"created_at": created_at,
|
||||
"completed_at": created_at,
|
||||
"output_text": "Hello",
|
||||
"output": [{
|
||||
"type": "message",
|
||||
"id": "resp_stream_001_msg",
|
||||
|
||||
@@ -112,7 +112,6 @@ fn auth_repository_with_redaction_feature_settings() -> Arc<InMemoryAuthApiKeySn
|
||||
Some(json!({
|
||||
"chat_pii_redaction": {
|
||||
"enabled": true,
|
||||
"inject_model_instruction": true,
|
||||
}
|
||||
})),
|
||||
)]),
|
||||
|
||||
@@ -468,9 +468,38 @@ async fn gateway_executes_codex_cli_stream_via_local_decision_gate_after_oauth_r
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let response_body =
|
||||
strip_sse_keepalive_comments(&response.text().await.expect("body should read"));
|
||||
let data_line = response_body
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("data: "))
|
||||
.expect("completed event data should exist");
|
||||
let completed_event: serde_json::Value =
|
||||
serde_json::from_str(data_line).expect("completed event should parse");
|
||||
let created_at = completed_event["response"]["created_at"]
|
||||
.as_i64()
|
||||
.expect("created_at should be a unix timestamp");
|
||||
|
||||
assert_eq!(
|
||||
strip_sse_keepalive_comments(&response.text().await.expect("body should read")),
|
||||
"event: response.completed\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_codex_cli_stream_local_123\",\"object\":\"response\",\"model\":\"gpt-5.4\",\"status\":\"completed\",\"usage\":{\"input_tokens\":1,\"output_tokens\":2,\"total_tokens\":3}}}\n\n"
|
||||
completed_event,
|
||||
json!({
|
||||
"type": "response.completed",
|
||||
"response": {
|
||||
"id": "resp_codex_cli_stream_local_123",
|
||||
"object": "response",
|
||||
"model": "gpt-5.4",
|
||||
"status": "completed",
|
||||
"usage": {
|
||||
"input_tokens": 1,
|
||||
"output_tokens": 2,
|
||||
"total_tokens": 3
|
||||
},
|
||||
"output": [],
|
||||
"created_at": created_at,
|
||||
"completed_at": created_at,
|
||||
"output_text": ""
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let seen_refresh_request = seen_refresh
|
||||
|
||||
@@ -112,7 +112,6 @@ async fn proxy_pii_redaction_local_openai_chat_runtime_masks_headers_and_restore
|
||||
Some(json!({
|
||||
"chat_pii_redaction": {
|
||||
"enabled": true,
|
||||
"inject_model_instruction": true,
|
||||
}
|
||||
})),
|
||||
)]),
|
||||
@@ -363,13 +362,7 @@ async fn proxy_pii_redaction_local_openai_chat_runtime_masks_headers_and_restore
|
||||
let provider_body_text = serde_json::to_string(&seen.body).expect("body should serialize");
|
||||
assert!(!provider_body_text.contains("[email protected]"));
|
||||
assert!(provider_body_text.contains("<AETHER:EMAIL:"));
|
||||
assert_eq!(seen.body["messages"][0]["role"], "assistant");
|
||||
let notice = seen.body["messages"][0]["content"]
|
||||
.as_str()
|
||||
.expect("notice should be text");
|
||||
assert!(notice.contains("not a user request"));
|
||||
assert!(notice.contains("do not answer"));
|
||||
assert_eq!(seen.body["messages"][1]["role"], "user");
|
||||
assert_eq!(seen.body["messages"][0]["role"], "user");
|
||||
|
||||
let stored_candidates = request_candidate_repository
|
||||
.list_by_request_id("trace-proxy-pii-redaction-sync")
|
||||
|
||||
@@ -230,14 +230,10 @@ fn redaction_test_rules() -> serde_json::Value {
|
||||
])
|
||||
}
|
||||
|
||||
fn chat_pii_redaction_feature_settings(
|
||||
enabled: bool,
|
||||
inject_model_instruction: bool,
|
||||
) -> serde_json::Value {
|
||||
fn chat_pii_redaction_feature_settings(enabled: bool) -> serde_json::Value {
|
||||
json!({
|
||||
"chat_pii_redaction": {
|
||||
"enabled": enabled,
|
||||
"inject_model_instruction": inject_model_instruction,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -245,7 +241,6 @@ fn chat_pii_redaction_feature_settings(
|
||||
fn auth_repository_with_redaction_feature_settings(
|
||||
test_id: &str,
|
||||
feature_enabled: bool,
|
||||
inject_model_instruction: bool,
|
||||
) -> Arc<InMemoryAuthApiKeySnapshotRepository> {
|
||||
let snapshot = auth_snapshot(&format!("api-key-{test_id}"), &format!("user-{test_id}"));
|
||||
let key_hash = hash_api_key(&format!("sk-client-{test_id}"));
|
||||
@@ -257,10 +252,7 @@ fn auth_repository_with_redaction_feature_settings(
|
||||
.with_export_records(vec![auth_export_record(
|
||||
&snapshot,
|
||||
key_hash,
|
||||
Some(chat_pii_redaction_feature_settings(
|
||||
feature_enabled,
|
||||
inject_model_instruction,
|
||||
)),
|
||||
Some(chat_pii_redaction_feature_settings(feature_enabled)),
|
||||
)]),
|
||||
)
|
||||
}
|
||||
@@ -372,8 +364,7 @@ async fn run_sync_redaction_case_with_system_config(
|
||||
}),
|
||||
);
|
||||
let (provider_url, provider_handle) = start_server(provider_app).await;
|
||||
let auth_repository =
|
||||
auth_repository_with_redaction_feature_settings(test_id, feature_enabled, true);
|
||||
let auth_repository = auth_repository_with_redaction_feature_settings(test_id, feature_enabled);
|
||||
let candidate_selection_repository =
|
||||
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||
candidate_row(test_id),
|
||||
@@ -522,14 +513,9 @@ async fn ai_execute_sync_pii_redaction_round_trip_impl() {
|
||||
assert!(provider_body_text.contains("<AETHER:ACCESS_TOKEN:"));
|
||||
assert!(provider_body_text.contains("<AETHER:SECRET_KEY:"));
|
||||
assert_eq!(seen.body["messages"][0]["role"], "system");
|
||||
assert_eq!(seen.body["messages"][1]["role"], "assistant");
|
||||
let notice = seen.body["messages"][1]["content"]
|
||||
.as_str()
|
||||
.expect("notice should be text");
|
||||
assert!(notice.contains("not a user request"));
|
||||
assert_eq!(seen.body["messages"][2]["role"], "user");
|
||||
assert_eq!(seen.body["messages"][3]["role"], "assistant");
|
||||
assert_eq!(seen.body["messages"][4]["role"], "tool");
|
||||
assert_eq!(seen.body["messages"][1]["role"], "user");
|
||||
assert_eq!(seen.body["messages"][2]["role"], "assistant");
|
||||
assert_eq!(seen.body["messages"][3]["role"], "tool");
|
||||
|
||||
let response_content = response_json["choices"][0]["message"]["content"]
|
||||
.as_str()
|
||||
@@ -702,7 +688,7 @@ async fn ai_execute_pii_redaction_restores_executed_candidate_session_after_late
|
||||
);
|
||||
let (provider_url, provider_handle) = start_server(provider_app).await;
|
||||
let auth_repository =
|
||||
auth_repository_with_redaction_feature_settings("redaction-candidate-session", true, true);
|
||||
auth_repository_with_redaction_feature_settings("redaction-candidate-session", true);
|
||||
let mut later_candidate = candidate_row("redaction-candidate-session");
|
||||
later_candidate.provider_id = "provider-redaction-candidate-session-later".to_string();
|
||||
later_candidate.endpoint_id = "endpoint-redaction-candidate-session-later".to_string();
|
||||
@@ -817,7 +803,7 @@ async fn pii_redaction_performance_limits_do_not_forward_unredacted_body_upstrea
|
||||
);
|
||||
let (provider_url, provider_handle) = start_server(provider_app).await;
|
||||
let auth_repository =
|
||||
auth_repository_with_redaction_feature_settings("pii-redaction-limit", true, true);
|
||||
auth_repository_with_redaction_feature_settings("pii-redaction-limit", true);
|
||||
let candidate_selection_repository =
|
||||
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||
candidate_row("pii-redaction-limit"),
|
||||
@@ -893,7 +879,7 @@ async fn ai_execute_pii_redaction_missing_encryption_key_fails_closed_before_pro
|
||||
);
|
||||
let (execution_runtime_url, execution_runtime_handle) = start_server(execution_runtime).await;
|
||||
let test_id = "ai-execute-pii-redaction-missing-encryption-key";
|
||||
let auth_repository = auth_repository_with_redaction_feature_settings(test_id, true, true);
|
||||
let auth_repository = auth_repository_with_redaction_feature_settings(test_id, true);
|
||||
let candidate_selection_repository =
|
||||
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||||
candidate_row(test_id),
|
||||
|
||||
@@ -498,8 +498,7 @@ fn auth_repository(case: &RedactionFormatCase) -> Arc<InMemoryAuthApiKeySnapshot
|
||||
key_hash,
|
||||
Some(json!({
|
||||
"chat_pii_redaction": {
|
||||
"enabled": true,
|
||||
"inject_model_instruction": true
|
||||
"enabled": true
|
||||
}
|
||||
})),
|
||||
)]),
|
||||
|
||||
@@ -117,6 +117,48 @@ fn admin_provider_oauth_complete_dispatch_remains_thin() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn postgres_provider_cleanup_preserves_usage_history() {
|
||||
let postgres_provider_catalog =
|
||||
read_workspace_file("crates/aether-data/src/repository/provider_catalog/postgres.rs");
|
||||
|
||||
for forbidden in [
|
||||
"UPDATE usage SET provider_id = NULL",
|
||||
"UPDATE usage SET provider_endpoint_id = NULL",
|
||||
"UPDATE usage SET provider_api_key_id = NULL",
|
||||
] {
|
||||
assert!(
|
||||
!postgres_provider_catalog.contains(forbidden),
|
||||
"provider cleanup must not rewrite usage history with {forbidden}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_cleanup_keeps_common_backends_in_sync() {
|
||||
for path in [
|
||||
"crates/aether-data/src/repository/provider_catalog/postgres.rs",
|
||||
"crates/aether-data/src/repository/provider_catalog/mysql.rs",
|
||||
"crates/aether-data/src/repository/provider_catalog/sqlite.rs",
|
||||
] {
|
||||
let source = read_workspace_file(path);
|
||||
for required in [
|
||||
"UPDATE user_preferences SET default_provider_id = NULL WHERE default_provider_id =",
|
||||
"UPDATE video_tasks SET provider_id = NULL WHERE provider_id =",
|
||||
"DELETE FROM request_candidates WHERE provider_id =",
|
||||
"UPDATE video_tasks SET endpoint_id = NULL WHERE endpoint_id =",
|
||||
"DELETE FROM request_candidates WHERE endpoint_id =",
|
||||
"DELETE FROM gemini_file_mappings WHERE key_id =",
|
||||
"UPDATE video_tasks SET key_id = NULL WHERE key_id =",
|
||||
] {
|
||||
assert!(
|
||||
source.contains(required),
|
||||
"{path} should keep provider cleanup behavior in sync with {required}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_provider_oauth_complete_helpers_are_split() {
|
||||
let complete_mod = read_workspace_file(
|
||||
|
||||
@@ -2506,7 +2506,7 @@ fn ai_serving_same_format_provider_root_request_separates_body_and_url_policy()
|
||||
"apps/aether-gateway/src/ai_serving/planner/passthrough/provider/family/request.rs",
|
||||
);
|
||||
for pattern in [
|
||||
"super::super::request::build_same_format_provider_request_body(",
|
||||
"super::super::request::build_same_format_provider_request_body_with_compatibility_report(",
|
||||
"super::super::request::build_same_format_upstream_url(",
|
||||
] {
|
||||
assert!(
|
||||
|
||||
@@ -5403,7 +5403,7 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
|
||||
assert!(stored_key.oauth_invalid_at_unix_secs.is_some());
|
||||
assert_eq!(
|
||||
stored_key.oauth_invalid_reason.as_deref(),
|
||||
Some("[OAUTH_EXPIRED] Codex Token 无效或已过期 (401)")
|
||||
Some("[OAUTH_EXPIRED] Codex Token 已过期 (401)")
|
||||
);
|
||||
} else if account_state_recheck_attempted
|
||||
&& payload["account_state_recheck_error"] == "wham/usage API 返回状态码 403"
|
||||
@@ -5449,7 +5449,40 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
|
||||
oauth_snapshot.get("expires_at"),
|
||||
auth_config.get("expires_at")
|
||||
);
|
||||
if stored_key.oauth_invalid_reason.is_some() {
|
||||
if stored_key
|
||||
.oauth_invalid_reason
|
||||
.as_deref()
|
||||
.is_some_and(|reason| reason.starts_with("[OAUTH_EXPIRED]"))
|
||||
{
|
||||
assert_eq!(
|
||||
oauth_snapshot
|
||||
.get("code")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("expired")
|
||||
);
|
||||
assert_eq!(
|
||||
oauth_snapshot
|
||||
.get("label")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
Some("已过期")
|
||||
);
|
||||
assert_eq!(
|
||||
oauth_snapshot.get("reason"),
|
||||
Some(&json!("Codex Token 已过期 (401)"))
|
||||
);
|
||||
assert_eq!(
|
||||
oauth_snapshot
|
||||
.get("requires_reauth")
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
Some(false)
|
||||
);
|
||||
assert_eq!(
|
||||
oauth_snapshot
|
||||
.get("expiring_soon")
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
Some(false)
|
||||
);
|
||||
} else if stored_key.oauth_invalid_reason.is_some() {
|
||||
assert_eq!(
|
||||
oauth_snapshot
|
||||
.get("code")
|
||||
@@ -5463,8 +5496,11 @@ async fn gateway_refreshes_admin_provider_oauth_key_locally_with_trusted_admin_p
|
||||
Some("已失效")
|
||||
);
|
||||
assert_eq!(
|
||||
oauth_snapshot.get("reason"),
|
||||
Some(&json!("Codex Token 无效或已过期 (401)"))
|
||||
oauth_snapshot
|
||||
.get("reason")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|reason| !reason.trim().is_empty()),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
oauth_snapshot
|
||||
|
||||
@@ -3526,6 +3526,15 @@ async fn gateway_cleans_up_admin_pool_banned_keys_locally_with_trusted_admin_pri
|
||||
);
|
||||
banned_key.name = "banned".to_string();
|
||||
banned_key.oauth_invalid_reason = Some("account_banned".to_string());
|
||||
let mut oauth_invalidated_key = sample_key(
|
||||
"key-openai-oauth-invalidated",
|
||||
"provider-openai",
|
||||
"openai:chat",
|
||||
"sk-oauth-invalidated",
|
||||
);
|
||||
oauth_invalidated_key.name = "oauth-invalidated".to_string();
|
||||
oauth_invalidated_key.oauth_invalid_reason =
|
||||
Some("[OAUTH_EXPIRED] token invalidated".to_string());
|
||||
let mut oauth_expired_key = sample_key(
|
||||
"key-openai-oauth-expired",
|
||||
"provider-openai",
|
||||
@@ -3533,7 +3542,7 @@ async fn gateway_cleans_up_admin_pool_banned_keys_locally_with_trusted_admin_pri
|
||||
"sk-oauth-expired",
|
||||
);
|
||||
oauth_expired_key.name = "oauth-expired".to_string();
|
||||
oauth_expired_key.oauth_invalid_reason = Some("[OAUTH_EXPIRED] token invalidated".to_string());
|
||||
oauth_expired_key.oauth_invalid_reason = Some("[OAUTH_EXPIRED] session expired".to_string());
|
||||
let mut healthy_key = sample_key(
|
||||
"key-openai-healthy",
|
||||
"provider-openai",
|
||||
@@ -3545,7 +3554,12 @@ async fn gateway_cleans_up_admin_pool_banned_keys_locally_with_trusted_admin_pri
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
Vec::new(),
|
||||
vec![banned_key, oauth_expired_key, healthy_key],
|
||||
vec![
|
||||
banned_key,
|
||||
oauth_invalidated_key,
|
||||
oauth_expired_key,
|
||||
healthy_key,
|
||||
],
|
||||
));
|
||||
|
||||
let (upstream_url, upstream_handle) = start_server(upstream).await;
|
||||
@@ -3575,8 +3589,8 @@ async fn gateway_cleans_up_admin_pool_banned_keys_locally_with_trusted_admin_pri
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["affected"], 1);
|
||||
assert_eq!(payload["message"], "已清理 1 个异常账号");
|
||||
assert_eq!(payload["affected"], 2);
|
||||
assert_eq!(payload["message"], "已清理 2 个异常账号");
|
||||
|
||||
let remaining_keys = provider_catalog_repository
|
||||
.list_keys_by_provider_ids(&["provider-openai".to_string()])
|
||||
@@ -3586,6 +3600,9 @@ async fn gateway_cleans_up_admin_pool_banned_keys_locally_with_trusted_admin_pri
|
||||
assert!(remaining_keys
|
||||
.iter()
|
||||
.any(|key| key.id == "key-openai-oauth-expired"));
|
||||
assert!(!remaining_keys
|
||||
.iter()
|
||||
.any(|key| key.id == "key-openai-oauth-invalidated"));
|
||||
assert!(remaining_keys
|
||||
.iter()
|
||||
.any(|key| key.id == "key-openai-healthy"));
|
||||
|
||||
@@ -1703,6 +1703,52 @@ async fn gateway_submits_admin_provider_delete_task_locally_with_trusted_admin_p
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_delete_task_reservation_reuses_active_provider_task() {
|
||||
let state = AppState::new().expect("gateway should build");
|
||||
let first = crate::LocalProviderDeleteTaskState {
|
||||
task_id: "task-first".to_string(),
|
||||
provider_id: "provider-openai".to_string(),
|
||||
status: "pending".to_string(),
|
||||
stage: "queued".to_string(),
|
||||
total_keys: 0,
|
||||
deleted_keys: 0,
|
||||
total_endpoints: 0,
|
||||
deleted_endpoints: 0,
|
||||
message: "delete task submitted".to_string(),
|
||||
};
|
||||
let second = crate::LocalProviderDeleteTaskState {
|
||||
task_id: "task-second".to_string(),
|
||||
provider_id: "provider-openai".to_string(),
|
||||
status: "pending".to_string(),
|
||||
stage: "queued".to_string(),
|
||||
total_keys: 0,
|
||||
deleted_keys: 0,
|
||||
total_endpoints: 0,
|
||||
deleted_endpoints: 0,
|
||||
message: "delete task submitted".to_string(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
state.reserve_provider_delete_task(first.clone()).task_id,
|
||||
"task-first"
|
||||
);
|
||||
assert_eq!(
|
||||
state.reserve_provider_delete_task(second.clone()).task_id,
|
||||
"task-first"
|
||||
);
|
||||
|
||||
state.put_provider_delete_task(crate::LocalProviderDeleteTaskState {
|
||||
status: "completed".to_string(),
|
||||
stage: "completed".to_string(),
|
||||
..first
|
||||
});
|
||||
assert_eq!(
|
||||
state.reserve_provider_delete_task(second).task_id,
|
||||
"task-second"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn local_admin_provider_delete_task_status_attaches_audit_only_for_terminal_states() {
|
||||
let mut completed_state = AppState::new().expect("gateway should build");
|
||||
|
||||
@@ -1172,7 +1172,7 @@ async fn gateway_handles_admin_usage_active_ids_for_terminal_updates() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_usage_records_locally_with_trusted_admin_principal() {
|
||||
let (upstream_url, upstream_hits, upstream_handle) =
|
||||
let (_upstream_url, upstream_hits, upstream_handle) =
|
||||
start_usage_upstream("/api/admin/usage/records").await;
|
||||
|
||||
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![
|
||||
@@ -1336,6 +1336,100 @@ async fn gateway_filters_admin_usage_records_with_unknown_model_or_provider() {
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_supports_fast_admin_usage_record_totals() {
|
||||
let (upstream_url, upstream_hits, upstream_handle) =
|
||||
start_usage_upstream("/api/admin/usage/records").await;
|
||||
|
||||
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![
|
||||
sample_usage_row(
|
||||
"usage-a",
|
||||
"req-a",
|
||||
Some("user-1"),
|
||||
Some("key-1"),
|
||||
Some("primary"),
|
||||
"OpenAI",
|
||||
"gpt-5",
|
||||
"completed",
|
||||
120,
|
||||
30,
|
||||
0.3,
|
||||
0.36,
|
||||
DAY_2_UNIX_SECS,
|
||||
),
|
||||
sample_usage_row(
|
||||
"usage-b",
|
||||
"req-b",
|
||||
Some("user-1"),
|
||||
Some("key-1"),
|
||||
Some("primary"),
|
||||
"OpenAI",
|
||||
"gpt-5-mini",
|
||||
"completed",
|
||||
80,
|
||||
20,
|
||||
0.2,
|
||||
0.24,
|
||||
DAY_2_UNIX_SECS - 1,
|
||||
),
|
||||
sample_usage_row(
|
||||
"usage-c",
|
||||
"req-c",
|
||||
Some("user-1"),
|
||||
Some("key-1"),
|
||||
Some("primary"),
|
||||
"Anthropic",
|
||||
"claude-sonnet",
|
||||
"completed",
|
||||
60,
|
||||
10,
|
||||
0.1,
|
||||
0.12,
|
||||
DAY_1_UNIX_SECS,
|
||||
),
|
||||
]));
|
||||
let gateway = build_router_with_state(
|
||||
AppState::new()
|
||||
.expect("gateway should build")
|
||||
.with_data_state_for_tests(GatewayDataState::with_usage_reader_for_tests(
|
||||
usage_repository,
|
||||
)),
|
||||
);
|
||||
let (gateway_url, gateway_handle) = start_server(gateway).await;
|
||||
|
||||
let fast_response = admin_request(reqwest::Client::new().get(format!(
|
||||
"{gateway_url}/api/admin/usage/records?start_date=2024-03-21&end_date=2024-03-22&tz_offset_minutes=0&include_total=false&limit=2&offset=0"
|
||||
)))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(fast_response.status(), StatusCode::OK);
|
||||
let fast_payload: serde_json::Value =
|
||||
fast_response.json().await.expect("json body should parse");
|
||||
assert_eq!(fast_payload["records"].as_array().unwrap().len(), 2);
|
||||
assert_eq!(fast_payload["total"], 3);
|
||||
assert_eq!(fast_payload["total_is_estimated"], true);
|
||||
|
||||
let total_response = admin_request(reqwest::Client::new().get(format!(
|
||||
"{gateway_url}/api/admin/usage/records?start_date=2024-03-21&end_date=2024-03-22&tz_offset_minutes=0&total_only=true&limit=2&offset=0"
|
||||
)))
|
||||
.send()
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(total_response.status(), StatusCode::OK);
|
||||
let total_payload: serde_json::Value =
|
||||
total_response.json().await.expect("json body should parse");
|
||||
assert_eq!(total_payload["records"].as_array().unwrap().len(), 0);
|
||||
assert_eq!(total_payload["total"], 3);
|
||||
assert_eq!(total_payload["total_is_estimated"], false);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
upstream_handle.abort();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn gateway_handles_admin_usage_records_with_provider_key_name_fallback_from_request_metadata()
|
||||
{
|
||||
|
||||
@@ -4999,8 +4999,7 @@ async fn gateway_updates_users_me_detail_locally_without_proxying_upstream() {
|
||||
"username": "alice-updated",
|
||||
"feature_settings": {
|
||||
"chat_pii_redaction": {
|
||||
"enabled": true,
|
||||
"inject_model_instruction": false
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -5034,10 +5033,6 @@ async fn gateway_updates_users_me_detail_locally_without_proxying_upstream() {
|
||||
get_payload["feature_settings"]["chat_pii_redaction"]["enabled"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
get_payload["feature_settings"]["chat_pii_redaction"]["inject_model_instruction"],
|
||||
false
|
||||
);
|
||||
assert_eq!(*upstream_hits.lock().expect("mutex should lock"), 0);
|
||||
|
||||
gateway_handle.abort();
|
||||
@@ -7167,8 +7162,7 @@ async fn gateway_handles_users_me_api_key_writes_locally_without_proxying_upstre
|
||||
"concurrent_limit": 4,
|
||||
"feature_settings": {
|
||||
"chat_pii_redaction": {
|
||||
"enabled": true,
|
||||
"inject_model_instruction": false
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}))
|
||||
@@ -7187,10 +7181,6 @@ async fn gateway_handles_users_me_api_key_writes_locally_without_proxying_upstre
|
||||
update_payload["feature_settings"]["chat_pii_redaction"]["enabled"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
update_payload["feature_settings"]["chat_pii_redaction"]["inject_model_instruction"],
|
||||
false
|
||||
);
|
||||
assert_eq!(update_payload["message"], "API密钥已更新");
|
||||
|
||||
let toggle_response = client
|
||||
|
||||
Reference in New Issue
Block a user