mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-04 16:37:46 +08:00
fix(ci): stabilize cross-platform workflow checks
This commit is contained in:
@@ -2307,8 +2307,8 @@ mod tests {
|
||||
transport.provider.provider_type = "antigravity".to_string();
|
||||
transport.endpoint.base_url = "https://antigravity.googleapis.com".to_string();
|
||||
transport.endpoint.custom_path = None;
|
||||
transport.key.auth_type = "oauth".to_string();
|
||||
transport.key.decrypted_api_key = "__placeholder__".to_string();
|
||||
transport.key.auth_type = "bearer".to_string();
|
||||
transport.key.decrypted_api_key = "imported-antigravity-token".to_string();
|
||||
transport.key.upstream_metadata = None;
|
||||
transport.key.decrypted_auth_config = Some(
|
||||
json!({
|
||||
|
||||
@@ -8,9 +8,9 @@ const ANTHROPIC_PRECOMMIT_MAX_WAIT: Duration = Duration::from_millis(750);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum StreamCommitPolicy {
|
||||
OnResponseHeaders,
|
||||
OnFirstClassifiedBody,
|
||||
OnFirstAnthropicSemanticEvent {
|
||||
ResponseHeaders,
|
||||
FirstClassifiedBody,
|
||||
FirstAnthropicSemanticEvent {
|
||||
max_bytes: usize,
|
||||
max_wait: Duration,
|
||||
},
|
||||
@@ -28,11 +28,11 @@ impl StreamCommitPolicy {
|
||||
force_prefetch: bool,
|
||||
) -> Self {
|
||||
if !has_direct_finalize {
|
||||
return Self::OnFirstClassifiedBody;
|
||||
return Self::FirstClassifiedBody;
|
||||
}
|
||||
|
||||
if force_prefetch {
|
||||
return Self::OnFirstClassifiedBody;
|
||||
return Self::FirstClassifiedBody;
|
||||
}
|
||||
|
||||
let content_type = content_type
|
||||
@@ -46,50 +46,50 @@ impl StreamCommitPolicy {
|
||||
&& !has_private_stream_normalizer
|
||||
&& !has_local_stream_rewriter
|
||||
{
|
||||
return Self::OnFirstAnthropicSemanticEvent {
|
||||
return Self::FirstAnthropicSemanticEvent {
|
||||
max_bytes: MAX_STREAM_PREFETCH_BYTES,
|
||||
max_wait: ANTHROPIC_PRECOMMIT_MAX_WAIT,
|
||||
};
|
||||
}
|
||||
return Self::OnResponseHeaders;
|
||||
return Self::ResponseHeaders;
|
||||
}
|
||||
|
||||
if has_private_stream_normalizer || has_local_stream_rewriter {
|
||||
return Self::OnFirstClassifiedBody;
|
||||
return Self::FirstClassifiedBody;
|
||||
}
|
||||
|
||||
if !provider_api_format.eq_ignore_ascii_case(client_api_format) {
|
||||
return Self::OnFirstClassifiedBody;
|
||||
return Self::FirstClassifiedBody;
|
||||
}
|
||||
|
||||
if content_type.is_empty() {
|
||||
return Self::OnResponseHeaders;
|
||||
return Self::ResponseHeaders;
|
||||
}
|
||||
|
||||
if content_type.contains("json") || content_type.ends_with("+json") {
|
||||
Self::OnFirstClassifiedBody
|
||||
Self::FirstClassifiedBody
|
||||
} else {
|
||||
Self::OnResponseHeaders
|
||||
Self::ResponseHeaders
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const fn commits_on_response_headers(self) -> bool {
|
||||
matches!(self, Self::OnResponseHeaders)
|
||||
matches!(self, Self::ResponseHeaders)
|
||||
}
|
||||
|
||||
pub(super) const fn requires_bounded_frame_wait(self) -> bool {
|
||||
matches!(self, Self::OnFirstAnthropicSemanticEvent { .. })
|
||||
matches!(self, Self::FirstAnthropicSemanticEvent { .. })
|
||||
}
|
||||
|
||||
pub(super) const fn max_precommit_wait(self) -> Option<Duration> {
|
||||
match self {
|
||||
Self::OnFirstAnthropicSemanticEvent { max_wait, .. } => Some(max_wait),
|
||||
Self::OnResponseHeaders | Self::OnFirstClassifiedBody => None,
|
||||
Self::FirstAnthropicSemanticEvent { max_wait, .. } => Some(max_wait),
|
||||
Self::ResponseHeaders | Self::FirstClassifiedBody => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) const fn is_native_anthropic(self) -> bool {
|
||||
matches!(self, Self::OnFirstAnthropicSemanticEvent { .. })
|
||||
matches!(self, Self::FirstAnthropicSemanticEvent { .. })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,8 +143,7 @@ impl StreamCommitGate {
|
||||
return StreamPrecommitObservation::Commit;
|
||||
}
|
||||
|
||||
let StreamCommitPolicy::OnFirstAnthropicSemanticEvent { max_bytes, .. } = self.policy
|
||||
else {
|
||||
let StreamCommitPolicy::FirstAnthropicSemanticEvent { max_bytes, .. } = self.policy else {
|
||||
return StreamPrecommitObservation::Pending;
|
||||
};
|
||||
|
||||
@@ -341,7 +340,7 @@ mod tests {
|
||||
};
|
||||
|
||||
fn native_anthropic_policy() -> StreamCommitPolicy {
|
||||
StreamCommitPolicy::OnFirstAnthropicSemanticEvent {
|
||||
StreamCommitPolicy::FirstAnthropicSemanticEvent {
|
||||
max_bytes: 16_384,
|
||||
max_wait: Duration::from_millis(750),
|
||||
}
|
||||
@@ -480,7 +479,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn transport_fragment_count_does_not_commit_an_incomplete_anthropic_error() {
|
||||
let policy = StreamCommitPolicy::OnFirstAnthropicSemanticEvent {
|
||||
let policy = StreamCommitPolicy::FirstAnthropicSemanticEvent {
|
||||
max_bytes: 1024,
|
||||
max_wait: Duration::from_millis(750),
|
||||
};
|
||||
|
||||
@@ -4041,8 +4041,8 @@ async fn execute_execution_runtime_stream_inner(
|
||||
frame_stream,
|
||||
stream_precommit_committed,
|
||||
provider_pool_in_flight_guard.take(),
|
||||
retry_scope_out.as_deref_mut(),
|
||||
retry_fallback_out.as_deref_mut(),
|
||||
retry_scope_out,
|
||||
retry_fallback_out,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -573,7 +573,7 @@ impl ProviderTransferStateTracker {
|
||||
now: Instant,
|
||||
) -> Option<ProviderTransferLimitReached> {
|
||||
if self.exhausted_provider_ids.contains(&plan.provider_id) {
|
||||
return Some(self.reached_snapshot(plan.provider_id.as_str(), now, false, false)?);
|
||||
return self.reached_snapshot(plan.provider_id.as_str(), now, false, false);
|
||||
}
|
||||
|
||||
let state = self.by_provider.get(&plan.provider_id)?;
|
||||
|
||||
@@ -7,7 +7,5 @@ mod task;
|
||||
|
||||
pub(super) use orchestration::handle_admin_provider_oauth_batch_import;
|
||||
pub(super) use parse::build_admin_provider_oauth_batch_task_state;
|
||||
pub(super) use task::{
|
||||
handle_admin_provider_oauth_start_agent_identity_import_task,
|
||||
handle_admin_provider_oauth_start_batch_import_task,
|
||||
};
|
||||
pub(super) use task::handle_admin_provider_oauth_start_agent_identity_import_task;
|
||||
pub(super) use task::handle_admin_provider_oauth_start_batch_import_task;
|
||||
|
||||
@@ -1611,7 +1611,9 @@ async fn proxy_request_inner(
|
||||
let buffered_body = buffered_body
|
||||
.as_ref()
|
||||
.expect("execution runtime/control auth gate should have buffered request body");
|
||||
let stream_request = request_wants_stream(&request_context, &parts.headers, buffered_body);
|
||||
let stream_request = control_decision.is_some_and(|decision| {
|
||||
owner_forward_request_is_stream(&parts, decision, buffered_body)
|
||||
});
|
||||
let mut local_execution_exhaustion = None;
|
||||
if stream_request {
|
||||
let execute_stream_started_at = Instant::now();
|
||||
|
||||
@@ -3915,7 +3915,9 @@ mod tests {
|
||||
|
||||
repository.failing.store(false, Ordering::Release);
|
||||
tokio::time::timeout(Duration::from_secs(2), async {
|
||||
while runtime.metrics.pending_current.load(Ordering::Acquire) != 0 {
|
||||
while runtime.metrics.pending_current.load(Ordering::Acquire) != 0
|
||||
|| runtime.normal_admission.available_permits() != 2
|
||||
{
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
|
||||
@@ -383,7 +383,7 @@ async fn gateway_executes_codex_search_with_responses_permission_and_search_cont
|
||||
.header(TRACE_ID_HEADER, "trace-search-1")
|
||||
.json(&json!({
|
||||
"id": "session-search-1",
|
||||
"model": "gpt-5.6-sol-ultra-fast",
|
||||
"model": "gpt-5.6-sol-ultra",
|
||||
"reasoning": {"effort": "low", "summary": "auto"},
|
||||
"input": "find current OpenAI documentation",
|
||||
"commands": {
|
||||
|
||||
@@ -2133,7 +2133,7 @@ fn admin_provider_oauth_dispatch_batch_mod_stays_thin() {
|
||||
"pub(super) fn apply_admin_provider_oauth_batch_import_hints(",
|
||||
"pub(super) async fn extract_admin_provider_oauth_batch_error_detail(",
|
||||
"pub(super) fn build_admin_provider_oauth_batch_import_response(",
|
||||
"pub(super) fn build_admin_provider_oauth_batch_task_state(",
|
||||
"pub(in super::super) fn build_admin_provider_oauth_batch_task_state(",
|
||||
] {
|
||||
assert!(
|
||||
batch_parse.contains(pattern),
|
||||
|
||||
@@ -4994,6 +4994,8 @@ fn retired_api_format_occurrences_are_whitelisted() {
|
||||
"crates/aether-data/runtime/src/lifecycle/migrate/tests.rs",
|
||||
"crates/aether-usage/runtime/src/report.rs",
|
||||
"frontend/src/api/endpoints/types/__tests__/api-format.spec.ts",
|
||||
"frontend/src/views/admin/module-management/modelDirectivesConfig.ts",
|
||||
"frontend/src/views/admin/module-management/__tests__/modelDirectivesConfig.spec.ts",
|
||||
];
|
||||
let allowed = allowed_paths
|
||||
.into_iter()
|
||||
|
||||
@@ -1750,7 +1750,8 @@ fn usage_repositories_are_owned_by_contracts_and_driver_adapters() {
|
||||
}
|
||||
let mysql_facade =
|
||||
read_workspace_file("crates/aether-data/runtime/src/repository/usage/mysql.rs");
|
||||
assert!(mysql_facade.contains("aether_data_mysql::MysqlUsageStorage"));
|
||||
assert!(mysql_facade.contains("use aether_data_mysql::"));
|
||||
assert!(mysql_facade.contains("MysqlUsageStorage"));
|
||||
for forbidden in ["sqlx::query", "FROM `usage`", "INSERT INTO `usage`"] {
|
||||
assert!(
|
||||
!mysql_facade.contains(forbidden),
|
||||
|
||||
@@ -332,7 +332,8 @@ fn testkit_gateway_harness_is_opt_in() {
|
||||
"aether-gateway = { workspace = true, features = [\"testkit\"], optional = true }"
|
||||
));
|
||||
|
||||
let testkit_lib = read_workspace_file("crates/aether-testing/testkit/src/lib.rs");
|
||||
let testkit_lib =
|
||||
read_workspace_file("crates/aether-testing/testkit/src/lib.rs").replace("\r\n", "\n");
|
||||
for module in ["execution_runtime", "gateway", "tunnel"] {
|
||||
assert!(
|
||||
testkit_lib.contains(&format!("#[cfg(feature = \"gateway\")]\nmod {module};")),
|
||||
|
||||
@@ -2828,12 +2828,14 @@ async fn gateway_handles_admin_keys_grouped_by_format_locally_with_trusted_admin
|
||||
.expect("Agent Identity auth config should encrypt"),
|
||||
);
|
||||
|
||||
let mut codex_provider = sample_provider("provider-codex", "codex", 30);
|
||||
codex_provider.provider_type = "codex".to_string();
|
||||
let provider_catalog_repository = Arc::new(SummaryNullingProviderCatalogReadRepository::seed(
|
||||
vec![
|
||||
sample_provider("provider-openai", "openai", 10),
|
||||
sample_provider("provider-claude", "claude", 20)
|
||||
.with_transport_fields(false, false, true, None, None, None, None, None, None),
|
||||
sample_provider("provider-codex", "codex", 30),
|
||||
codex_provider,
|
||||
],
|
||||
vec![
|
||||
sample_endpoint(
|
||||
|
||||
@@ -474,12 +474,19 @@ async fn gateway_auto_removes_codex_key_when_quota_proves_oauth_invalid() {
|
||||
"stale-access-token",
|
||||
);
|
||||
key.auth_type = "oauth".to_string();
|
||||
key.expires_at_unix_secs = Some(1);
|
||||
key.expires_at_unix_secs = Some(4_102_444_800);
|
||||
key.oauth_invalid_at_unix_secs = Some(1);
|
||||
key.oauth_invalid_reason = Some(
|
||||
"[REFRESH_FAILED] Token 续期失败 (401): refresh_token 无效、已过期或已撤销,请重新登录授权"
|
||||
.to_string(),
|
||||
);
|
||||
key.encrypted_auth_config = Some(
|
||||
encrypt_python_fernet_plaintext(
|
||||
DEVELOPMENT_ENCRYPTION_KEY,
|
||||
r#"{"provider_type":"codex","refresh_token":"invalid-refresh-token","expires_at":4102444800}"#,
|
||||
)
|
||||
.expect("auth config should encrypt"),
|
||||
);
|
||||
|
||||
let provider_catalog_repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||||
vec![provider],
|
||||
@@ -521,7 +528,7 @@ async fn gateway_auto_removes_codex_key_when_quota_proves_oauth_invalid() {
|
||||
let payload: serde_json::Value = response.json().await.expect("json body should parse");
|
||||
assert_eq!(payload["success"], 0);
|
||||
assert_eq!(payload["failed"], 1);
|
||||
assert_eq!(payload["auto_removed"], 1);
|
||||
assert_eq!(payload["auto_removed"], 1, "payload={payload}");
|
||||
assert_eq!(payload["results"][0]["status"], "auth_invalid");
|
||||
assert_eq!(payload["results"][0]["auto_removed"], true);
|
||||
|
||||
|
||||
@@ -152,25 +152,33 @@ fn sample_kiro_device_access_token_without_email() -> String {
|
||||
}
|
||||
|
||||
fn sample_codex_access_token_with_profile_email(email: &str, account_id: &str) -> String {
|
||||
sample_codex_access_token_with_profile_email_and_expiry(email, account_id, Some(2_000_000_000))
|
||||
}
|
||||
|
||||
fn sample_codex_access_token_with_profile_email_and_expiry(
|
||||
email: &str,
|
||||
account_id: &str,
|
||||
expires_at: Option<u64>,
|
||||
) -> String {
|
||||
use base64::Engine as _;
|
||||
|
||||
let header =
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(r#"{"alg":"none","typ":"JWT"}"#);
|
||||
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
|
||||
json!({
|
||||
"iss": "https://auth.openai.com",
|
||||
"aud": ["https://api.openai.com/v1"],
|
||||
"exp": 2_000_000_000u64,
|
||||
"https://api.openai.com/profile": {
|
||||
"email": email,
|
||||
"email_verified": true,
|
||||
},
|
||||
"https://api.openai.com/auth": {
|
||||
"chatgpt_account_id": account_id,
|
||||
},
|
||||
})
|
||||
.to_string(),
|
||||
);
|
||||
let mut claims = json!({
|
||||
"iss": "https://auth.openai.com",
|
||||
"aud": ["https://api.openai.com/v1"],
|
||||
"https://api.openai.com/profile": {
|
||||
"email": email,
|
||||
"email_verified": true,
|
||||
},
|
||||
"https://api.openai.com/auth": {
|
||||
"chatgpt_account_id": account_id,
|
||||
},
|
||||
});
|
||||
if let Some(expires_at) = expires_at {
|
||||
claims["exp"] = json!(expires_at);
|
||||
}
|
||||
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(claims.to_string());
|
||||
format!("{header}.{payload}.sig")
|
||||
}
|
||||
|
||||
@@ -4352,6 +4360,11 @@ fn gateway_imports_codex_access_token_with_payload_expires_at_when_token_has_no_
|
||||
}
|
||||
|
||||
async fn gateway_imports_codex_access_token_with_payload_expires_at_when_token_has_no_exp_impl() {
|
||||
let access_token = sample_codex_access_token_with_profile_email_and_expiry(
|
||||
"[email protected]",
|
||||
"acct-opaque-123",
|
||||
None,
|
||||
);
|
||||
let mut provider = sample_provider("provider-codex", "codex", 10);
|
||||
provider.provider_type = "codex".to_string();
|
||||
let endpoint = sample_endpoint(
|
||||
@@ -4387,7 +4400,7 @@ async fn gateway_imports_codex_access_token_with_payload_expires_at_when_token_h
|
||||
.header(TRUSTED_ADMIN_USER_ROLE_HEADER, "admin")
|
||||
.header(TRUSTED_ADMIN_SESSION_ID_HEADER, "session-123")
|
||||
.json(&json!({
|
||||
"access_token": "opaque-codex-access-token",
|
||||
"access_token": access_token,
|
||||
"expiresAt": 2_100_000_000u64,
|
||||
"name": "temporary-codex-opaque-access-token",
|
||||
}))
|
||||
@@ -7763,14 +7776,14 @@ async fn gateway_consecutive_manual_oauth_refresh_uses_rotated_refresh_token_imp
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_concurrent_manual_oauth_refresh_uses_rotated_refresh_token_after_lock_wait() {
|
||||
fn gateway_concurrent_manual_oauth_refresh_reuses_winner_after_lock_wait() {
|
||||
run_admin_oauth_test(
|
||||
"gateway_concurrent_manual_oauth_refresh_uses_rotated_refresh_token_after_lock_wait",
|
||||
gateway_concurrent_manual_oauth_refresh_uses_rotated_refresh_token_after_lock_wait_impl,
|
||||
"gateway_concurrent_manual_oauth_refresh_reuses_winner_after_lock_wait",
|
||||
gateway_concurrent_manual_oauth_refresh_reuses_winner_after_lock_wait_impl,
|
||||
);
|
||||
}
|
||||
|
||||
async fn gateway_concurrent_manual_oauth_refresh_uses_rotated_refresh_token_after_lock_wait_impl() {
|
||||
async fn gateway_concurrent_manual_oauth_refresh_reuses_winner_after_lock_wait_impl() {
|
||||
let refresh_request_bodies = Arc::new(Mutex::new(Vec::<String>::new()));
|
||||
let refresh_request_bodies_clone = Arc::clone(&refresh_request_bodies);
|
||||
let execution_runtime = Router::new().route(
|
||||
@@ -7963,17 +7976,12 @@ async fn gateway_concurrent_manual_oauth_refresh_uses_rotated_refresh_token_afte
|
||||
.lock()
|
||||
.expect("mutex should lock")
|
||||
.clone();
|
||||
assert_eq!(bodies.len(), 2);
|
||||
assert_eq!(bodies.len(), 1);
|
||||
assert!(
|
||||
bodies[0].contains("refresh_token=old-codex-refresh-token"),
|
||||
"unexpected first refresh body: {}",
|
||||
bodies[0]
|
||||
);
|
||||
assert!(
|
||||
bodies[1].contains("refresh_token=rotated-codex-refresh-token"),
|
||||
"unexpected second refresh body: {}",
|
||||
bodies[1]
|
||||
);
|
||||
|
||||
let stored_key = provider_catalog_repository
|
||||
.list_keys_by_ids(&["key-codex-oauth-concurrent-refresh".to_string()])
|
||||
@@ -7992,10 +8000,7 @@ async fn gateway_concurrent_manual_oauth_refresh_uses_rotated_refresh_token_afte
|
||||
.expect("auth config should decrypt");
|
||||
let auth_config: serde_json::Value =
|
||||
serde_json::from_str(&decrypted_auth_config).expect("auth config should parse");
|
||||
assert_eq!(
|
||||
auth_config["refresh_token"],
|
||||
"rotated-codex-refresh-token-2"
|
||||
);
|
||||
assert_eq!(auth_config["refresh_token"], "rotated-codex-refresh-token");
|
||||
|
||||
gateway_handle.abort();
|
||||
execution_runtime_handle.abort();
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::sync::Arc;
|
||||
use aether_contracts::{ExecutionPlan, ExecutionResult, ResponseBody};
|
||||
use aether_crypto::DEVELOPMENT_ENCRYPTION_KEY;
|
||||
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
||||
use base64::Engine as _;
|
||||
use http::StatusCode;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -634,7 +635,17 @@ fn assert_native_gemini_embedding_execution_plan(plan: &ExecutionPlan) {
|
||||
Some("gemini-embedding-2-preview")
|
||||
);
|
||||
assert!(!plan.stream);
|
||||
let body = plan.body.json_body.as_ref().expect("json request body");
|
||||
assert!(plan.body.json_body.is_none());
|
||||
let body_bytes = base64::engine::general_purpose::STANDARD
|
||||
.decode(
|
||||
plan.body
|
||||
.body_bytes_b64
|
||||
.as_deref()
|
||||
.expect("original request body bytes"),
|
||||
)
|
||||
.expect("request body should decode");
|
||||
let body: serde_json::Value =
|
||||
serde_json::from_slice(&body_bytes).expect("request body should parse");
|
||||
assert_eq!(body["content"]["parts"][0]["text"], "hello");
|
||||
assert!(body.get("input").is_none());
|
||||
assert!(body.get("messages").is_none());
|
||||
|
||||
@@ -1203,6 +1203,7 @@ mod tests {
|
||||
#[test]
|
||||
fn to_config_enables_pretty_file_logging_with_defaults() {
|
||||
let mut app = sample_app();
|
||||
set_global_field(&mut app, "install_service", "false");
|
||||
set_global_field(&mut app, "save_logs_to_file", "true");
|
||||
|
||||
let cfg = app.to_config().expect("config should serialize");
|
||||
|
||||
Reference in New Issue
Block a user