mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 01:47:47 +08:00
1115 lines
37 KiB
Rust
1115 lines
37 KiB
Rust
use std::sync::Arc;
|
|||
use std::time::{Duration, Instant};
|
|||
|
|||
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
|
|||
use aether_data::repository::auth::{
|
|||
|
|
InMemoryAuthApiKeySnapshotRepository, StoredAuthApiKeySnapshot,
|
||
|
|
};
|
||
use aether_data::repository::candidate_selection::InMemoryMinimalCandidateSelectionReadRepository;
|
|||
|
|
use aether_data::repository::candidates::InMemoryRequestCandidateRepository;
|
||
|
|
use aether_data::repository::provider_catalog::InMemoryProviderCatalogReadRepository;
|
||
|
|
use aether_data::repository::usage::InMemoryUsageReadRepository;
|
||
use aether_data::repository::users::{
|
|||
|
|
InMemoryUserReadRepository, StoredUserAuthRecord, StoredUserPreferenceRecord,
|
||
|
|
};
|
||
use aether_data::repository::video_tasks::InMemoryVideoTaskRepository;
|
|||
use aether_data::{DataLayerError, DatabaseDriver, SqlDatabaseConfig, SqlPoolConfig};
|
|||
use aether_data_contracts::repository::candidate_selection::{
|
|||
|
|
StoredMinimalCandidateSelectionRow, StoredProviderModelMapping,
|
||
|
|
};
|
||
|
|
use aether_data_contracts::repository::candidates::{
|
||
|
|
RequestCandidateStatus, StoredRequestCandidate,
|
||
|
|
};
|
||
|
|
use aether_data_contracts::repository::provider_catalog::{
|
||
|
|
StoredProviderCatalogEndpoint, StoredProviderCatalogKey, StoredProviderCatalogProvider,
|
||
|
|
};
|
||
|
|
use aether_data_contracts::repository::usage::StoredRequestUsageAudit;
|
||
|
|
use aether_data_contracts::repository::video_tasks::{
|
||
|
|
UpsertVideoTask, VideoTaskLookupKey, VideoTaskStatus, VideoTaskWriteRepository,
|
||
|
|
};
|
||
use aether_scheduler_core::{
|
|||
enumerate_minimal_candidate_selection, EnumerateMinimalCandidateSelectionInput,
|
|||
|
|
SchedulerAuthConstraints,
|
||
};
|
|||
use serde_json::json;
|
|||
|
|||
|
|
use super::{GatewayDataConfig, GatewayDataState};
|
||
use crate::AppState;
|
|||
|
|||
|
|
#[test]
|
||
|
|
fn disabled_gateway_data_state_has_no_backends() {
|
||
|
|
let state = GatewayDataState::from_config(GatewayDataConfig::disabled())
|
||
|
|
.expect("disabled config should build");
|
||
|
|
|
||
|
|
assert!(!state.has_backends());
|
||
|
|
assert!(!state.has_auth_api_key_reader());
|
||
assert!(!state.has_minimal_candidate_selection_reader());
|
|||
assert!(!state.has_request_candidate_reader());
|
|||
|
|
assert!(!state.has_provider_catalog_reader());
|
||
assert!(!state.has_proxy_node_reader());
|
|||
|
|
assert!(!state.has_proxy_node_writer());
|
||
assert!(!state.has_usage_reader());
|
|||
|
|
assert!(!state.has_video_task_reader());
|
||
|
|
}
|
||
|
|
|
||
#[test]
|
|||
|
|
fn maintenance_pool_pressure_keeps_idle_reserve_for_foreground_work() {
|
||
let pool_can_still_grow = aether_data::DatabasePoolSummary {
|
|||
driver: DatabaseDriver::Postgres,
|
|||
|
|
checked_out: 6,
|
||
|
|
pool_size: 6,
|
||
|
|
idle: 0,
|
||
|
|
max_connections: 20,
|
||
|
|
usage_rate: 30.0,
|
||
|
|
};
|
||
assert!(
|
|||
|
|
!GatewayDataState::database_pool_summary_under_maintenance_pressure(&pool_can_still_grow)
|
||
|
|
);
|
||
|
|||
let reserve_idle_left = aether_data::DatabasePoolSummary {
|
|||
driver: DatabaseDriver::Postgres,
|
|||
checked_out: 18,
|
|||
|
|
pool_size: 20,
|
||
|
|
idle: 2,
|
||
max_connections: 20,
|
|||
usage_rate: 90.0,
|
|||
};
|
|||
assert!(GatewayDataState::database_pool_summary_under_maintenance_pressure(&reserve_idle_left));
|
|||
|
|
|
||
|
|
let above_idle_reserve = aether_data::DatabasePoolSummary {
|
||
|
|
driver: DatabaseDriver::Postgres,
|
||
|
|
checked_out: 17,
|
||
|
|
pool_size: 20,
|
||
|
|
idle: 3,
|
||
|
|
max_connections: 20,
|
||
|
|
usage_rate: 85.0,
|
||
|
|
};
|
||
|
|
assert!(
|
||
|
|
!GatewayDataState::database_pool_summary_under_maintenance_pressure(&above_idle_reserve)
|
||
|
|
);
|
||
|
|||
|
|
let idle = aether_data::DatabasePoolSummary {
|
||
|
|
driver: DatabaseDriver::Postgres,
|
||
|
|
checked_out: 0,
|
||
|
|
pool_size: 4,
|
||
|
|
idle: 4,
|
||
|
|
max_connections: 20,
|
||
|
|
usage_rate: 0.0,
|
||
|
|
};
|
||
|
|
assert!(!GatewayDataState::database_pool_summary_under_maintenance_pressure(&idle));
|
||
|
|
}
|
||
|
|
|
||
#[test]
|
|||
|
|
fn usage_worker_pool_pressure_only_defers_near_pool_exhaustion() {
|
||
|
|
let comfortable = aether_data::DatabasePoolSummary {
|
||
|
|
driver: DatabaseDriver::Postgres,
|
||
|
|
checked_out: 56,
|
||
|
|
pool_size: 64,
|
||
|
|
idle: 8,
|
||
|
|
max_connections: 64,
|
||
|
|
usage_rate: 87.5,
|
||
|
|
};
|
||
|
|
assert!(!GatewayDataState::database_pool_summary_under_usage_worker_pressure(&comfortable));
|
||
|
|
|
||
|
|
let last_idle_left = aether_data::DatabasePoolSummary {
|
||
|
|
driver: DatabaseDriver::Postgres,
|
||
|
|
checked_out: 63,
|
||
|
|
pool_size: 64,
|
||
|
|
idle: 1,
|
||
|
|
max_connections: 64,
|
||
|
|
usage_rate: 98.4375,
|
||
|
|
};
|
||
|
|
assert!(GatewayDataState::database_pool_summary_under_usage_worker_pressure(&last_idle_left));
|
||
|
|
|
||
|
|
let exhausted = aether_data::DatabasePoolSummary {
|
||
|
|
driver: DatabaseDriver::Postgres,
|
||
|
|
checked_out: 64,
|
||
|
|
pool_size: 64,
|
||
|
|
idle: 0,
|
||
|
|
max_connections: 64,
|
||
|
|
usage_rate: 100.0,
|
||
|
|
};
|
||
|
|
assert!(GatewayDataState::database_pool_summary_under_usage_worker_pressure(&exhausted));
|
||
|
|
}
|
||
|
|
|
||
#[test]
|
|||
|
|
fn maintenance_pool_pressure_deferral_has_timeout() {
|
||
|
|
let mut deferred_since = None;
|
||
|
|
assert!(
|
||
|
|
GatewayDataState::should_defer_maintenance_for_pool_pressure_state(
|
||
|
|
true,
|
||
|
|
&mut deferred_since
|
||
|
|
)
|
||
|
|
);
|
||
|
|
assert!(deferred_since.is_some());
|
||
|
|
|
||
|
|
assert!(
|
||
|
|
!GatewayDataState::should_defer_maintenance_for_pool_pressure_state(
|
||
|
|
false,
|
||
|
|
&mut deferred_since
|
||
|
|
)
|
||
|
|
);
|
||
|
|
assert!(deferred_since.is_none());
|
||
|
|
|
||
|
|
let mut stale_defer = Some(Instant::now() - Duration::from_secs(31));
|
||
|
|
assert!(
|
||
|
|
!GatewayDataState::should_defer_maintenance_for_pool_pressure_state(true, &mut stale_defer)
|
||
|
|
);
|
||
|
|
assert!(stale_defer.is_none());
|
||
|
|
}
|
||
|
|
|
||
#[tokio::test]
|
|||
|
|
async fn postgres_gateway_data_state_builds_video_task_reader() {
|
||
|
|
let state = GatewayDataState::from_config(GatewayDataConfig::from_postgres_url(
|
||
|
|
"postgres://localhost/aether",
|
||
|
|
false,
|
||
|
|
))
|
||
|
|
.expect("postgres-backed state should build");
|
||
|
|
|
||
|
|
assert!(state.has_backends());
|
||
|
|
assert!(state.has_auth_api_key_reader());
|
||
assert!(state.has_minimal_candidate_selection_reader());
|
|||
assert!(state.has_request_candidate_reader());
|
|||
|
|
assert!(state.has_provider_catalog_reader());
|
||
assert!(state.has_proxy_node_reader());
|
|||
|
|
assert!(state.has_proxy_node_writer());
|
||
assert!(state.has_usage_reader());
|
|||
|
|
assert!(state.has_video_task_reader());
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn data_state_find_uses_configured_read_repository() {
|
||
|
|
let repository = Arc::new(InMemoryVideoTaskRepository::default());
|
||
|
|
repository
|
||
|
|
.upsert(UpsertVideoTask {
|
||
|
|
id: "task-1".to_string(),
|
||
|
|
short_id: Some("short-task-1".to_string()),
|
||
request_id: "request-1".to_string(),
|
|||
user_id: Some("user-1".to_string()),
|
|||
api_key_id: Some("key-1".to_string()),
|
|||
|
|
username: Some("user".to_string()),
|
||
|
|
api_key_name: Some("primary".to_string()),
|
||
external_task_id: Some("ext-task-1".to_string()),
|
|||
provider_id: Some("provider-1".to_string()),
|
|||
|
|
endpoint_id: Some("endpoint-1".to_string()),
|
||
|
|
key_id: Some("provider-key-1".to_string()),
|
||
|
|
client_api_format: Some("openai:video".to_string()),
|
||
provider_api_format: Some("openai:video".to_string()),
|
|||
format_converted: false,
|
|||
model: Some("sora-2".to_string()),
|
|||
|
|
prompt: Some("hello".to_string()),
|
||
original_request_body: Some(json!({"prompt": "hello"})),
|
|||
|
|
duration_seconds: Some(4),
|
||
|
|
resolution: Some("720p".to_string()),
|
||
|
|
aspect_ratio: Some("16:9".to_string()),
|
||
size: Some("1280x720".to_string()),
|
|||
|
|
status: VideoTaskStatus::Queued,
|
||
|
|
progress_percent: 0,
|
||
progress_message: None,
|
|||
|
|
retry_count: 0,
|
||
|
|
poll_interval_seconds: 10,
|
||
|
|
next_poll_at_unix_secs: Some(100),
|
||
|
|
poll_count: 0,
|
||
|
|
max_poll_count: 360,
|
||
created_at_unix_ms: 100,
|
|||
submitted_at_unix_secs: Some(100),
|
|||
|
|
completed_at_unix_secs: None,
|
||
updated_at_unix_secs: 100,
|
|||
|
|
error_code: None,
|
||
|
|
error_message: None,
|
||
|
|
video_url: None,
|
||
request_metadata: None,
|
|||
})
|
|||
|
|
.await
|
||
|
|
.expect("upsert should succeed");
|
||
|
|
|
||
|
|
let state = GatewayDataState::with_video_task_reader_for_tests(repository);
|
||
|
|
|
||
|
|
let task = state
|
||
|
|
.find_video_task(VideoTaskLookupKey::Id("task-1"))
|
||
|
|
.await
|
||
|
|
.expect("find should succeed");
|
||
|
|
|
||
|
|
assert_eq!(task.expect("task should exist").id, "task-1");
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn app_state_wires_gateway_data_state_from_config() {
|
||
let state = AppState::new()
|
|||
.expect("app state should build")
|
|||
|
|
.with_data_config(GatewayDataConfig::from_postgres_url(
|
||
|
|
"postgres://localhost/aether",
|
||
|
|
false,
|
||
|
|
))
|
||
|
|
.expect("data config should wire");
|
||
|
|||
|
|
assert!(state.data.has_backends());
|
||
|
|
assert!(state.data.has_auth_api_key_reader());
|
||
assert!(state.data.has_minimal_candidate_selection_reader());
|
|||
assert!(state.data.has_request_candidate_reader());
|
|||
|
|
assert!(state.data.has_provider_catalog_reader());
|
||
assert!(state.data.has_proxy_node_reader());
|
|||
|
|
assert!(state.data.has_proxy_node_writer());
|
||
assert!(state.data.has_usage_reader());
|
|||
|
|
assert!(state.data.has_video_task_reader());
|
||
|
|
}
|
||
|
|
|
||
#[tokio::test]
|
|||
|
|
async fn data_state_checks_user_uniqueness_through_user_reader() {
|
||
|
|
let user = StoredUserAuthRecord::new(
|
||
|
|
"user-1".to_string(),
|
||
|
|
Some("[email protected]".to_string()),
|
||
|
|
true,
|
||
|
|
"alice".to_string(),
|
||
|
|
Some("hash".to_string()),
|
||
|
|
"user".to_string(),
|
||
|
|
"local".to_string(),
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
true,
|
||
|
|
false,
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
)
|
||
|
|
.expect("auth user should build");
|
||
|
|
let admin = StoredUserAuthRecord::new(
|
||
|
|
"admin-1".to_string(),
|
||
|
|
Some("[email protected]".to_string()),
|
||
|
|
true,
|
||
|
|
"admin".to_string(),
|
||
Some("$2b$12$4qL4tdcsFwVaDTw5Ck3xzu8GpNdre56DiNR6Dnw7t6gCXaEnqAe7G".to_string()),
|
|||
"admin".to_string(),
|
|||
|
|
"local".to_string(),
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
true,
|
||
|
|
false,
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
)
|
||
|
|
.expect("admin user should build");
|
||
|
|
let state = GatewayDataState::with_user_reader_for_tests(Arc::new(
|
||
|
|
InMemoryUserReadRepository::seed_auth_users(vec![user, admin]),
|
||
|
|
));
|
||
|
|
|
||
|
|
assert!(state
|
||
|
|
.is_other_user_auth_email_taken("[email protected]", "other-user")
|
||
|
|
.await
|
||
|
|
.expect("email uniqueness should check"));
|
||
|
|
assert!(!state
|
||
|
|
.is_other_user_auth_email_taken("[email protected]", "user-1")
|
||
|
|
.await
|
||
|
|
.expect("same user email should not be taken"));
|
||
|
|
assert!(!state
|
||
|
|
.is_other_user_auth_email_taken("alice", "other-user")
|
||
|
|
.await
|
||
|
|
.expect("email lookup should not match username"));
|
||
|
|
assert!(state
|
||
|
|
.is_other_user_auth_username_taken("alice", "other-user")
|
||
|
|
.await
|
||
|
|
.expect("username uniqueness should check"));
|
||
|
|
assert_eq!(
|
||
|
|
state
|
||
|
|
.count_active_admin_users()
|
||
|
|
.await
|
||
|
|
.expect("active admin count should check"),
|
||
|
|
1
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
state
|
||
|
|
.count_active_local_admin_users_with_valid_password()
|
||
|
|
.await
|
||
|
|
.expect("valid local admin count should check"),
|
||
|
|
1
|
||
|
|
);
|
||
|
|
let preferences = StoredUserPreferenceRecord {
|
||
|
|
user_id: "user-1".to_string(),
|
||
|
|
avatar_url: Some("https://example.test/avatar.png".to_string()),
|
||
|
|
bio: Some("hello".to_string()),
|
||
|
|
default_provider_id: None,
|
||
|
|
default_provider_name: None,
|
||
|
|
theme: "dark".to_string(),
|
||
|
|
language: "en-US".to_string(),
|
||
|
|
timezone: "UTC".to_string(),
|
||
|
|
email_notifications: false,
|
||
|
|
usage_alerts: true,
|
||
|
|
announcement_notifications: false,
|
||
|
|
};
|
||
|
|
assert_eq!(
|
||
|
|
state
|
||
|
|
.write_user_preferences(&preferences)
|
||
|
|
.await
|
||
|
|
.expect("preferences should write through repository"),
|
||
|
|
Some(preferences.clone())
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
state
|
||
|
|
.read_user_preferences("user-1")
|
||
|
|
.await
|
||
|
|
.expect("preferences should read through repository"),
|
||
|
|
Some(preferences)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn data_state_finds_active_provider_name_through_catalog_reader() {
|
||
|
|
let active = StoredProviderCatalogProvider::new(
|
||
|
|
"provider-1".to_string(),
|
||
|
|
"Provider One".to_string(),
|
||
|
|
None,
|
||
|
|
"openai".to_string(),
|
||
|
|
)
|
||
|
|
.expect("provider should build");
|
||
|
|
let inactive = StoredProviderCatalogProvider::new(
|
||
|
|
"provider-2".to_string(),
|
||
|
|
"Provider Two".to_string(),
|
||
|
|
None,
|
||
|
|
"openai".to_string(),
|
||
|
|
)
|
||
|
|
.expect("provider should build")
|
||
|
|
.with_transport_fields(false, false, false, None, None, None, None, None, None);
|
||
|
|
let state = GatewayDataState::with_provider_catalog_reader_for_tests(Arc::new(
|
||
|
|
InMemoryProviderCatalogReadRepository::seed(vec![active, inactive], Vec::new(), Vec::new()),
|
||
|
|
));
|
||
|
|
|
||
|
|
assert_eq!(
|
||
|
|
state
|
||
|
|
.find_active_provider_name("provider-1")
|
||
|
|
.await
|
||
|
|
.expect("provider lookup should succeed"),
|
||
|
|
Some("Provider One".to_string())
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
state
|
||
|
|
.find_active_provider_name("provider-2")
|
||
|
|
.await
|
||
|
|
.expect("inactive provider lookup should succeed"),
|
||
|
|
None
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
fn sample_auth_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
|
|||
|
|
StoredAuthApiKeySnapshot::new(
|
||
|
|
user_id.to_string(),
|
||
|
|
"alice".to_string(),
|
||
|
|
Some("[email protected]".to_string()),
|
||
|
|
"user".to_string(),
|
||
|
|
"local".to_string(),
|
||
|
|
true,
|
||
|
|
false,
|
||
|
|
Some(serde_json::json!(["openai"])),
|
||
|
|
Some(serde_json::json!(["openai:chat"])),
|
||
|
|
Some(serde_json::json!(["gpt-4.1"])),
|
||
|
|
api_key_id.to_string(),
|
||
|
|
Some("default".to_string()),
|
||
|
|
true,
|
||
|
|
false,
|
||
|
|
false,
|
||
|
|
Some(60),
|
||
|
|
Some(5),
|
||
|
|
Some(200),
|
||
|
|
Some(serde_json::json!(["openai"])),
|
||
|
|
Some(serde_json::json!(["openai:chat"])),
|
||
|
|
Some(serde_json::json!(["gpt-4.1"])),
|
||
|
|
)
|
||
|
|
.expect("auth snapshot should build")
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn data_state_reads_auth_api_key_snapshot_from_reader() {
|
||
|
|
let repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||
|
|
Some("hash-1".to_string()),
|
||
|
|
sample_auth_snapshot("key-1", "user-1"),
|
||
|
|
)]));
|
||
|
|
let state = GatewayDataState::with_auth_api_key_reader_for_tests(repository);
|
||
|
|
|
||
|
|
let snapshot = state
|
||
|
|
.read_auth_api_key_snapshot("user-1", "key-1", 150)
|
||
|
|
.await
|
||
|
|
.expect("read should succeed")
|
||
|
|
.expect("snapshot should exist");
|
||
|
|
|
||
|
|
assert_eq!(snapshot.user_id, "user-1");
|
||
|
|
assert_eq!(snapshot.api_key_id, "key-1");
|
||
|
|
assert_eq!(snapshot.username, "alice");
|
||
|
|
assert_eq!(
|
||
|
|
snapshot.api_key_allowed_models,
|
||
|
|
Some(vec!["gpt-4.1".to_string()])
|
||
|
|
);
|
||
|
|
assert!(snapshot.currently_usable);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn sample_provider_catalog_provider() -> StoredProviderCatalogProvider {
|
||
|
|
StoredProviderCatalogProvider::new(
|
||
|
|
"provider-1".to_string(),
|
||
|
|
"OpenAI".to_string(),
|
||
|
|
Some("https://openai.com".to_string()),
|
||
|
|
"custom".to_string(),
|
||
|
|
)
|
||
|
|
.expect("provider should build")
|
||
|
|
}
|
||
|
|
|
||
|
|
fn sample_provider_catalog_endpoint() -> StoredProviderCatalogEndpoint {
|
||
|
|
StoredProviderCatalogEndpoint::new(
|
||
|
|
"endpoint-1".to_string(),
|
||
|
|
"provider-1".to_string(),
|
||
|
|
"openai:chat".to_string(),
|
||
|
|
Some("openai".to_string()),
|
||
|
|
Some("chat".to_string()),
|
||
|
|
true,
|
||
|
|
)
|
||
|
|
.expect("endpoint should build")
|
||
|
|
}
|
||
|
|
|
||
|
|
fn sample_provider_catalog_key() -> StoredProviderCatalogKey {
|
||
|
|
StoredProviderCatalogKey::new(
|
||
|
|
"provider-key-1".to_string(),
|
||
|
|
"provider-1".to_string(),
|
||
|
|
"prod-key".to_string(),
|
||
|
|
"api_key".to_string(),
|
||
|
|
Some(serde_json::json!({"cache_1h": true})),
|
||
|
|
true,
|
||
|
|
)
|
||
|
|
.expect("key should build")
|
||
|
|
}
|
||
|
|
|
||
|
|
fn sample_request_usage(request_id: &str) -> StoredRequestUsageAudit {
|
||
|
|
StoredRequestUsageAudit::new(
|
||
|
|
"usage-1".to_string(),
|
||
|
|
request_id.to_string(),
|
||
|
|
Some("user-1".to_string()),
|
||
|
|
Some("api-key-1".to_string()),
|
||
|
|
Some("alice".to_string()),
|
||
|
|
Some("default".to_string()),
|
||
|
|
"OpenAI".to_string(),
|
||
|
|
"gpt-4.1".to_string(),
|
||
|
|
Some("gpt-4.1-mini".to_string()),
|
||
|
|
Some("provider-1".to_string()),
|
||
|
|
Some("endpoint-1".to_string()),
|
||
|
|
Some("provider-key-1".to_string()),
|
||
|
|
Some("chat".to_string()),
|
||
|
|
Some("openai:chat".to_string()),
|
||
|
|
Some("openai".to_string()),
|
||
|
|
Some("chat".to_string()),
|
||
|
|
Some("openai:chat".to_string()),
|
||
|
|
Some("openai".to_string()),
|
||
|
|
Some("chat".to_string()),
|
||
|
|
true,
|
||
|
|
false,
|
||
|
|
120,
|
||
|
|
40,
|
||
|
|
160,
|
||
|
|
0.24,
|
||
|
|
0.36,
|
||
|
|
Some(200),
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
Some(450),
|
||
|
|
Some(120),
|
||
|
|
"completed".to_string(),
|
||
|
|
"settled".to_string(),
|
||
|
|
100,
|
||
|
|
101,
|
||
|
|
Some(102),
|
||
|
|
)
|
||
|
|
.expect("usage should build")
|
||
|
|
}
|
||
|
|
|
||
fn sample_minimal_candidate_selection_row(
|
|||
|
|
provider_id: &str,
|
||
|
|
provider_name: &str,
|
||
|
|
provider_priority: i32,
|
||
|
|
key_id: &str,
|
||
|
|
key_name: &str,
|
||
|
|
key_internal_priority: i32,
|
||
|
|
) -> StoredMinimalCandidateSelectionRow {
|
||
|
|
StoredMinimalCandidateSelectionRow {
|
||
|
|
provider_id: provider_id.to_string(),
|
||
|
|
provider_name: provider_name.to_string(),
|
||
|
|
provider_type: "custom".to_string(),
|
||
|
|
provider_priority,
|
||
|
|
provider_is_active: true,
|
||
|
|
endpoint_id: format!("endpoint-{provider_id}"),
|
||
|
|
endpoint_api_format: "openai:chat".to_string(),
|
||
|
|
endpoint_api_family: Some("openai".to_string()),
|
||
|
|
endpoint_kind: Some("chat".to_string()),
|
||
|
|
endpoint_is_active: true,
|
||
|
|
key_id: key_id.to_string(),
|
||
|
|
key_name: key_name.to_string(),
|
||
|
|
key_auth_type: "api_key".to_string(),
|
||
|
|
key_is_active: true,
|
||
|
|
key_api_formats: Some(vec!["openai:chat".to_string()]),
|
||
|
|
key_allowed_models: None,
|
||
|
|
key_capabilities: Some(serde_json::json!({"cache_1h": true})),
|
||
|
|
key_internal_priority,
|
||
|
|
key_global_priority_by_format: Some(serde_json::json!({"openai:chat": 3})),
|
||
|
|
model_id: format!("model-{provider_id}"),
|
||
|
|
global_model_id: "global-model-1".to_string(),
|
||
|
|
global_model_name: "gpt-4.1".to_string(),
|
||
|
|
global_model_mappings: Some(vec!["gpt-4\\.1-.*".to_string()]),
|
||
|
|
global_model_supports_streaming: Some(true),
|
||
|
|
model_provider_model_name: "gpt-4.1-upstream".to_string(),
|
||
|
|
model_provider_model_mappings: Some(vec![StoredProviderModelMapping {
|
||
|
|
name: "gpt-4.1-canary".to_string(),
|
||
|
|
priority: 1,
|
||
|
|
api_formats: Some(vec!["openai:chat".to_string()]),
|
||
endpoint_ids: None,
|
|||
operations: None,
|
|||
}]),
|
|||
|
|
model_supports_streaming: None,
|
||
|
|
model_is_active: true,
|
||
|
|
model_is_available: true,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
#[tokio::test]
|
|||
|
|
async fn data_state_reads_decision_trace_with_provider_catalog_metadata() {
|
||
|
|
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![
|
||
|
|
StoredRequestCandidate::new(
|
||
|
|
"cand-1".to_string(),
|
||
|
|
"req-1".to_string(),
|
||
|
|
Some("user-1".to_string()),
|
||
|
|
Some("api-key-1".to_string()),
|
||
|
|
Some("alice".to_string()),
|
||
|
|
Some("default".to_string()),
|
||
|
|
0,
|
||
|
|
0,
|
||
|
|
Some("provider-1".to_string()),
|
||
|
|
Some("endpoint-1".to_string()),
|
||
|
|
Some("provider-key-1".to_string()),
|
||
|
|
RequestCandidateStatus::Failed,
|
||
|
|
None,
|
||
|
|
false,
|
||
|
|
Some(502),
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
Some(37),
|
||
|
|
Some(1),
|
||
|
|
None,
|
||
|
|
Some(serde_json::json!({"cache_1h": true})),
|
||
100_000,
|
|||
|
|
Some(101_000),
|
||
|
|
Some(102_000),
|
||
)
|
|||
|
|
.expect("candidate should build"),
|
||
|
|
]));
|
||
|
|
let provider_catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||
|
|
vec![sample_provider_catalog_provider()],
|
||
|
|
vec![sample_provider_catalog_endpoint()],
|
||
|
|
vec![sample_provider_catalog_key()],
|
||
|
|
));
|
||
|
|
let state = GatewayDataState::with_decision_trace_readers_for_tests(
|
||
|
|
request_candidates,
|
||
|
|
provider_catalog,
|
||
|
|
);
|
||
|
|
|
||
|
|
let trace = state
|
||
|
|
.read_decision_trace("req-1", true)
|
||
|
|
.await
|
||
|
|
.expect("trace should read")
|
||
|
|
.expect("trace should exist");
|
||
|
|
|
||
|
|
assert_eq!(trace.request_id, "req-1");
|
||
|
|
assert_eq!(trace.total_candidates, 1);
|
||
|
|
assert_eq!(trace.candidates[0].provider_name.as_deref(), Some("OpenAI"));
|
||
|
|
assert_eq!(
|
||
|
|
trace.candidates[0].endpoint_api_format.as_deref(),
|
||
|
|
Some("openai:chat")
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
trace.candidates[0].provider_key_auth_type.as_deref(),
|
||
|
|
Some("api_key")
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
trace.candidates[0].provider_key_capabilities,
|
||
|
|
Some(serde_json::json!({"cache_1h": true}))
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn data_state_reads_request_usage_audit_from_reader() {
|
||
|
|
let repository = Arc::new(InMemoryUsageReadRepository::seed(vec![
|
||
|
|
sample_request_usage("req-usage-1"),
|
||
|
|
]));
|
||
|
|
let state = GatewayDataState::with_usage_reader_for_tests(repository);
|
||
|
|
|
||
|
|
let usage = state
|
||
|
|
.read_request_usage_audit("req-usage-1")
|
||
|
|
.await
|
||
|
|
.expect("read should succeed")
|
||
|
|
.expect("usage should exist");
|
||
|
|
|
||
assert_eq!(usage.request_id, "req-usage-1");
|
|||
|
|
assert_eq!(usage.provider_name, "OpenAI");
|
||
|
|
assert_eq!(usage.total_tokens, 160);
|
||
|
|
assert_eq!(usage.total_cost_usd, 0.24);
|
||
|
|
assert!(usage.has_format_conversion);
|
||
}
|
|||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn data_state_reads_request_audit_bundle_from_multiple_readers() {
|
||
|
|
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||
|
|
Some("hash-1".to_string()),
|
||
|
|
sample_auth_snapshot("api-key-1", "user-1"),
|
||
|
|
)]));
|
||
|
|
let request_candidates = Arc::new(InMemoryRequestCandidateRepository::seed(vec![
|
||
|
|
StoredRequestCandidate::new(
|
||
|
|
"cand-1".to_string(),
|
||
|
|
"req-usage-1".to_string(),
|
||
|
|
Some("user-1".to_string()),
|
||
|
|
Some("api-key-1".to_string()),
|
||
|
|
Some("alice".to_string()),
|
||
|
|
Some("default".to_string()),
|
||
|
|
0,
|
||
|
|
0,
|
||
|
|
Some("provider-1".to_string()),
|
||
|
|
Some("endpoint-1".to_string()),
|
||
|
|
Some("provider-key-1".to_string()),
|
||
|
|
RequestCandidateStatus::Success,
|
||
|
|
None,
|
||
|
|
false,
|
||
|
|
Some(200),
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
Some(37),
|
||
|
|
Some(1),
|
||
|
|
None,
|
||
|
|
Some(serde_json::json!({"cache_1h": true})),
|
||
100_000,
|
|||
|
|
Some(101_000),
|
||
|
|
Some(102_000),
|
||
)
|
|||
|
|
.expect("candidate should build"),
|
||
|
|
]));
|
||
|
|
let provider_catalog = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||
|
|
vec![sample_provider_catalog_provider()],
|
||
|
|
vec![sample_provider_catalog_endpoint()],
|
||
|
|
vec![sample_provider_catalog_key()],
|
||
|
|
));
|
||
|
|
let usage_repository = Arc::new(InMemoryUsageReadRepository::seed(vec![
|
||
|
|
sample_request_usage("req-usage-1"),
|
||
|
|
]));
|
||
|
|
let state = GatewayDataState::with_request_audit_readers_for_tests(
|
||
|
|
auth_repository,
|
||
|
|
request_candidates,
|
||
|
|
provider_catalog,
|
||
|
|
usage_repository,
|
||
|
|
);
|
||
|
|
|
||
|
|
let bundle = state
|
||
|
|
.read_request_audit_bundle("req-usage-1", true, 150)
|
||
|
|
.await
|
||
|
|
.expect("bundle should read")
|
||
|
|
.expect("bundle should exist");
|
||
|
|
|
||
|
|
assert_eq!(bundle.request_id, "req-usage-1");
|
||
|
|
assert_eq!(
|
||
|
|
bundle
|
||
|
|
.usage
|
||
|
|
.as_ref()
|
||
.and_then(|usage| usage.target_model.as_deref()),
|
|||
Some("gpt-4.1-mini")
|
|||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
bundle
|
||
|
|
.decision_trace
|
||
|
|
.as_ref()
|
||
|
|
.and_then(|trace| trace.candidates.first())
|
||
|
|
.and_then(|candidate| candidate.provider_name.as_deref()),
|
||
|
|
Some("OpenAI")
|
||
|
|
);
|
||
|
|
assert_eq!(
|
||
|
|
bundle
|
||
|
|
.auth_snapshot
|
||
|
|
.as_ref()
|
||
|
|
.map(|snapshot| snapshot.currently_usable),
|
||
|
|
Some(true)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
#[tokio::test]
|
|||
|
|
async fn data_state_reads_decrypted_provider_transport_snapshot() {
|
||
|
|
let encrypted_api_key =
|
||
|
|
encrypt_python_fernet_plaintext(DEVELOPMENT_ENCRYPTION_KEY, "sk-live-openai")
|
||
|
|
.expect("api key ciphertext should build");
|
||
|
|
let encrypted_auth_config = encrypt_python_fernet_plaintext(
|
||
|
|
DEVELOPMENT_ENCRYPTION_KEY,
|
||
|
|
"{\"refresh_token\":\"rt-1\",\"project\":\"demo\"}",
|
||
|
|
)
|
||
|
|
.expect("auth config ciphertext should build");
|
||
|
|
let provider = sample_provider_catalog_provider().with_transport_fields(
|
||
|
|
true,
|
||
|
|
false,
|
||
|
|
true,
|
||
|
|
Some(32),
|
||
|
|
Some(3),
|
||
|
|
Some(serde_json::json!({"url":"http://provider-proxy"})),
|
||
|
|
Some(20.0),
|
||
|
|
Some(8.0),
|
||
|
|
Some(serde_json::json!({"region":"global"})),
|
||
|
|
);
|
||
|
|
let endpoint = sample_provider_catalog_endpoint()
|
||
|
|
.with_transport_fields(
|
||
|
|
"https://api.openai.com".to_string(),
|
||
|
|
Some(serde_json::json!([{"action":"set","key":"x-test","value":"1"}])),
|
||
|
|
Some(serde_json::json!([{"action":"drop","path":"stream"}])),
|
||
|
|
Some(2),
|
||
|
|
Some("/v1/chat/completions".to_string()),
|
||
|
|
Some(serde_json::json!({"api_version":"v1"})),
|
||
|
|
Some(serde_json::json!({"allow":["openai:chat"]})),
|
||
|
|
Some(serde_json::json!({"url":"http://endpoint-proxy"})),
|
||
|
|
)
|
||
|
|
.expect("endpoint transport should build");
|
||
|
|
let key = sample_provider_catalog_key()
|
||
|
|
.with_transport_fields(
|
||
Some(serde_json::json!(["openai:chat", "openai:responses"])),
|
|||
encrypted_api_key,
|
|||
|
|
Some(encrypted_auth_config),
|
||
|
|
Some(serde_json::json!({"openai:chat": 0.8})),
|
||
|
|
Some(serde_json::json!({"openai:chat": 1})),
|
||
|
|
Some(serde_json::json!(["gpt-4.1", "gpt-4.1-mini"])),
|
||
|
|
Some(1_800_000_000),
|
||
|
|
Some(serde_json::json!({"node_id":"proxy-node-1"})),
|
||
Some(serde_json::json!({"transport_profile":"chrome_136"})),
|
|||
)
|
|||
|
|
.expect("key transport should build");
|
||
|
|
let repository = Arc::new(InMemoryProviderCatalogReadRepository::seed(
|
||
|
|
vec![provider],
|
||
|
|
vec![endpoint],
|
||
|
|
vec![key],
|
||
|
|
));
|
||
|
|
let state = GatewayDataState::with_provider_transport_reader_for_tests(
|
||
|
|
repository,
|
||
|
|
DEVELOPMENT_ENCRYPTION_KEY.to_string(),
|
||
|
|
);
|
||
|
|
|
||
|
|
let snapshot = state
|
||
|
|
.read_provider_transport_snapshot("provider-1", "endpoint-1", "provider-key-1")
|
||
|
|
.await
|
||
|
|
.expect("snapshot read should succeed")
|
||
|
|
.expect("snapshot should exist");
|
||
|
|
|
||
|
|
assert_eq!(snapshot.provider.name, "OpenAI");
|
||
|
|
assert_eq!(snapshot.endpoint.base_url, "https://api.openai.com");
|
||
|
|
assert_eq!(
|
||
|
|
snapshot.key.api_formats,
|
||
Some(vec![
|
|||
|
|
"openai:chat".to_string(),
|
||
|
|
"openai:responses".to_string()
|
||
|
|
])
|
||
);
|
|||
|
|
assert_eq!(snapshot.key.decrypted_api_key, "sk-live-openai");
|
||
|
|
assert_eq!(
|
||
|
|
snapshot.key.decrypted_auth_config.as_deref(),
|
||
|
|
Some("{\"refresh_token\":\"rt-1\",\"project\":\"demo\"}")
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn data_state_reads_minimal_candidate_selection_with_auth_filters() {
|
||
|
|
let candidate_selection_repository =
|
||
|
|
Arc::new(InMemoryMinimalCandidateSelectionReadRepository::seed(vec![
|
||
|
|
sample_minimal_candidate_selection_row(
|
||
|
|
"provider-2",
|
||
|
|
"OtherProvider",
|
||
|
|
20,
|
||
|
|
"key-2",
|
||
|
|
"key-two",
|
||
|
|
20,
|
||
|
|
),
|
||
|
|
sample_minimal_candidate_selection_row(
|
||
|
|
"provider-1",
|
||
|
|
"OpenAI",
|
||
|
|
10,
|
||
|
|
"key-1",
|
||
|
|
"key-one",
|
||
|
|
10,
|
||
|
|
),
|
||
|
|
StoredMinimalCandidateSelectionRow {
|
||
|
|
key_global_priority_by_format: Some(serde_json::json!({"openai:chat": 4})),
|
||
|
|
key_allowed_models: Some(vec!["gpt-4.1-edge".to_string()]),
|
||
|
|
..sample_minimal_candidate_selection_row(
|
||
|
|
"provider-1",
|
||
|
|
"OpenAI",
|
||
|
|
10,
|
||
|
|
"key-3",
|
||
|
|
"key-three",
|
||
|
|
30,
|
||
|
|
)
|
||
|
|
},
|
||
|
|
]));
|
||
|
|
let auth_repository = Arc::new(InMemoryAuthApiKeySnapshotRepository::seed(vec![(
|
||
|
|
Some("hash-1".to_string()),
|
||
|
|
sample_auth_snapshot("api-key-1", "user-1"),
|
||
|
|
)]));
|
||
|
|
let state = GatewayDataState::with_minimal_candidate_selection_and_auth_for_tests(
|
||
|
|
candidate_selection_repository,
|
||
|
|
auth_repository,
|
||
|
|
);
|
||
|
|
let auth_snapshot = state
|
||
|
|
.read_auth_api_key_snapshot("user-1", "api-key-1", 150)
|
||
|
|
.await
|
||
|
|
.expect("auth snapshot should read")
|
||
|
|
.expect("auth snapshot should exist");
|
||
|
|
|
||
let rows = state
|
|||
|
|
.list_minimal_candidate_selection_rows("openai:chat", "gpt-4.1")
|
||
|
|
.await
|
||
|
|
.expect("minimal candidate selection rows should read");
|
||
|
|
let auth_constraints = SchedulerAuthConstraints {
|
||
|
|
allowed_providers: auth_snapshot
|
||
|
|
.effective_allowed_providers()
|
||
|
|
.map(|items| items.to_vec()),
|
||
|
|
allowed_api_formats: auth_snapshot
|
||
|
|
.effective_allowed_api_formats()
|
||
|
|
.map(|items| items.to_vec()),
|
||
|
|
allowed_models: auth_snapshot
|
||
|
|
.effective_allowed_models()
|
||
|
|
.map(|items| items.to_vec()),
|
||
|
|
};
|
||
|
|
|
||
let selection =
|
|||
|
|
enumerate_minimal_candidate_selection(EnumerateMinimalCandidateSelectionInput {
|
||
|
|
rows,
|
||
|
|
normalized_api_format: "openai:chat",
|
||
request_operation: None,
|
|||
requested_model_name: "gpt-4.1",
|
|||
|
|
resolved_global_model_name: "gpt-4.1",
|
||
|
|
require_streaming: false,
|
||
|
|
required_capabilities: None,
|
||
|
|
auth_constraints: Some(&auth_constraints),
|
||
|
|
})
|
||
|
|
.expect("selection should read");
|
||
|
|||
|
|
assert_eq!(selection.len(), 2);
|
||
|
|
assert_eq!(selection[0].provider_id, "provider-1");
|
||
|
|
assert_eq!(selection[0].selected_provider_model_name, "gpt-4.1-canary");
|
||
|
|
assert_eq!(selection[0].mapping_matched_model, None);
|
||
|
|
assert_eq!(selection[1].key_id, "key-3");
|
||
|
|
assert_eq!(
|
||
|
|
selection[1].selected_provider_model_name,
|
||
"gpt-4.1-edge".to_string()
|
|||
);
|
|||
|
|
assert_eq!(
|
||
|
|
selection[1].mapping_matched_model,
|
||
|
|
Some("gpt-4.1-edge".to_string())
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
#[tokio::test]
|
|||
|
|
async fn maps_openai_video_task_repository_row_into_read_response() {
|
||
|
|
let repository = Arc::new(InMemoryVideoTaskRepository::default());
|
||
|
|
repository
|
||
|
|
.upsert(UpsertVideoTask {
|
||
|
|
id: "task-1".to_string(),
|
||
|
|
short_id: Some("short-task-1".to_string()),
|
||
request_id: "request-1".to_string(),
|
|||
user_id: Some("user-1".to_string()),
|
|||
api_key_id: Some("key-1".to_string()),
|
|||
|
|
username: Some("user".to_string()),
|
||
|
|
api_key_name: Some("primary".to_string()),
|
||
external_task_id: Some("ext-task-1".to_string()),
|
|||
provider_id: Some("provider-1".to_string()),
|
|||
|
|
endpoint_id: Some("endpoint-1".to_string()),
|
||
|
|
key_id: Some("provider-key-1".to_string()),
|
||
|
|
client_api_format: Some("openai:video".to_string()),
|
||
provider_api_format: Some("openai:video".to_string()),
|
|||
format_converted: false,
|
|||
model: Some("sora-2".to_string()),
|
|||
|
|
prompt: Some("hello".to_string()),
|
||
original_request_body: Some(json!({"prompt": "hello"})),
|
|||
|
|
duration_seconds: Some(4),
|
||
|
|
resolution: Some("720p".to_string()),
|
||
|
|
aspect_ratio: Some("16:9".to_string()),
|
||
size: Some("1280x720".to_string()),
|
|||
|
|
status: VideoTaskStatus::Processing,
|
||
|
|
progress_percent: 45,
|
||
progress_message: Some("working".to_string()),
|
|||
|
|
retry_count: 0,
|
||
|
|
poll_interval_seconds: 10,
|
||
|
|
next_poll_at_unix_secs: Some(120),
|
||
|
|
poll_count: 1,
|
||
|
|
max_poll_count: 360,
|
||
created_at_unix_ms: 100,
|
|||
submitted_at_unix_secs: Some(100),
|
|||
|
|
completed_at_unix_secs: None,
|
||
updated_at_unix_secs: 120,
|
|||
|
|
error_code: None,
|
||
|
|
error_message: None,
|
||
|
|
video_url: None,
|
||
request_metadata: None,
|
|||
})
|
|||
|
|
.await
|
||
|
|
.expect("upsert should succeed");
|
||
|
|
|
||
|
|
let state = GatewayDataState::with_video_task_reader_for_tests(repository);
|
||
|
|
let response = state
|
||
|
|
.read_video_task_response(Some("openai"), "/v1/videos/task-1")
|
||
|
|
.await
|
||
|
|
.expect("read should succeed")
|
||
|
|
.expect("read response should exist");
|
||
|
|
|
||
|
|
assert_eq!(response.status_code, 200);
|
||
|
|
assert_eq!(response.body_json["id"], "task-1");
|
||
|
|
assert_eq!(response.body_json["status"], "processing");
|
||
|
|
assert_eq!(response.body_json["created_at"], 100);
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn maps_gemini_video_task_repository_row_into_read_response() {
|
||
|
|
let repository = Arc::new(InMemoryVideoTaskRepository::default());
|
||
|
|
repository
|
||
|
|
.upsert(UpsertVideoTask {
|
||
|
|
id: "task-1".to_string(),
|
||
|
|
short_id: Some("localshort123".to_string()),
|
||
request_id: "request-1".to_string(),
|
|||
user_id: Some("user-1".to_string()),
|
|||
api_key_id: Some("key-1".to_string()),
|
|||
|
|
username: Some("user".to_string()),
|
||
|
|
api_key_name: Some("primary".to_string()),
|
||
external_task_id: Some("operations/ext-task-1".to_string()),
|
|||
provider_id: Some("provider-1".to_string()),
|
|||
|
|
endpoint_id: Some("endpoint-1".to_string()),
|
||
|
|
key_id: Some("provider-key-1".to_string()),
|
||
|
|
client_api_format: Some("gemini:video".to_string()),
|
||
provider_api_format: Some("gemini:video".to_string()),
|
|||
format_converted: false,
|
|||
model: Some("veo-3".to_string()),
|
|||
|
|
prompt: Some("hello".to_string()),
|
||
original_request_body: Some(json!({"prompt": "hello"})),
|
|||
|
|
duration_seconds: Some(8),
|
||
|
|
resolution: Some("720p".to_string()),
|
||
|
|
aspect_ratio: Some("16:9".to_string()),
|
||
size: Some("720p".to_string()),
|
|||
|
|
status: VideoTaskStatus::Completed,
|
||
|
|
progress_percent: 100,
|
||
progress_message: None,
|
|||
|
|
retry_count: 0,
|
||
|
|
poll_interval_seconds: 10,
|
||
|
|
next_poll_at_unix_secs: None,
|
||
|
|
poll_count: 4,
|
||
|
|
max_poll_count: 360,
|
||
created_at_unix_ms: 100,
|
|||
submitted_at_unix_secs: Some(100),
|
|||
|
|
completed_at_unix_secs: Some(120),
|
||
updated_at_unix_secs: 120,
|
|||
|
|
error_code: None,
|
||
|
|
error_message: None,
|
||
|
|
video_url: None,
|
||
request_metadata: Some(json!({
|
|||
|
|
"rust_local_snapshot": {
|
||
|
|
"metadata": {
|
||
|
|
"generateVideoResponse": {
|
||
|
|
"generatedSamples": [
|
||
|
|
{
|
||
|
|
"video": {
|
||
|
|
"uri": "/v1beta/files/aev_localshort123:download?alt=media"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})),
|
||
})
|
|||
|
|
.await
|
||
|
|
.expect("upsert should succeed");
|
||
|
|
|
||
|
|
let state = GatewayDataState::with_video_task_reader_for_tests(repository);
|
||
|
|
let response = state
|
||
|
|
.read_video_task_response(
|
||
|
|
Some("gemini"),
|
||
|
|
"/v1beta/models/veo-3/operations/localshort123",
|
||
|
|
)
|
||
|
|
.await
|
||
|
|
.expect("read should succeed")
|
||
|
|
.expect("read response should exist");
|
||
|
|
|
||
|
|
assert_eq!(response.status_code, 200);
|
||
|
|
assert_eq!(
|
||
|
|
response.body_json["name"],
|
||
|
|
"models/veo-3/operations/localshort123"
|
||
|
|
);
|
||
|
|
assert_eq!(response.body_json["done"], true);
|
||
|
|
}
|
||
|
|
|
||
|
|
fn sample_request_candidate(
|
||
|
|
id: &str,
|
||
|
|
request_id: &str,
|
||
|
|
candidate_index: i32,
|
||
|
|
status: RequestCandidateStatus,
|
||
started_at_unix_ms: Option<i64>,
|
|||
latency_ms: Option<i32>,
|
|||
|
|
status_code: Option<i32>,
|
||
|
|
) -> StoredRequestCandidate {
|
||
|
|
StoredRequestCandidate::new(
|
||
|
|
id.to_string(),
|
||
|
|
request_id.to_string(),
|
||
|
|
Some("user-1".to_string()),
|
||
|
|
Some("api-key-1".to_string()),
|
||
|
|
Some("alice".to_string()),
|
||
|
|
Some("default".to_string()),
|
||
|
|
candidate_index,
|
||
|
|
0,
|
||
|
|
Some("provider-1".to_string()),
|
||
|
|
Some("endpoint-1".to_string()),
|
||
|
|
Some("provider-key-1".to_string()),
|
||
|
|
status,
|
||
|
|
None,
|
||
|
|
false,
|
||
|
|
status_code,
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
latency_ms,
|
||
|
|
Some(1),
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
100 + i64::from(candidate_index),
|
||
started_at_unix_ms,
|
|||
|
|
started_at_unix_ms.map(|value| value + 1),
|
||
)
|
|||
|
|
.expect("candidate should build")
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn data_state_reads_request_candidate_trace_from_reader() {
|
||
|
|
let repository = Arc::new(InMemoryRequestCandidateRepository::seed(vec![
|
||
|
|
sample_request_candidate(
|
||
|
|
"cand-1",
|
||
|
|
"req-1",
|
||
|
|
0,
|
||
|
|
RequestCandidateStatus::Pending,
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
None,
|
||
|
|
),
|
||
|
|
sample_request_candidate(
|
||
|
|
"cand-2",
|
||
|
|
"req-1",
|
||
|
|
1,
|
||
|
|
RequestCandidateStatus::Success,
|
||
|
|
Some(101),
|
||
|
|
Some(42),
|
||
|
|
Some(200),
|
||
|
|
),
|
||
|
|
]));
|
||
|
|
let state = GatewayDataState::with_request_candidate_reader_for_tests(repository);
|
||
|
|
|
||
|
|
let trace = state
|
||
|
|
.read_request_candidate_trace("req-1", true)
|
||
|
|
.await
|
||
|
|
.expect("trace should succeed")
|
||
|
|
.expect("trace should exist");
|
||
|
|
|
||
|
|
assert_eq!(trace.request_id, "req-1");
|
||
|
|
assert_eq!(trace.total_candidates, 1);
|
||
|
|
assert_eq!(
|
||
|
|
trace.final_status,
|
||
|
|
super::candidates::RequestCandidateFinalStatus::Success
|
||
|
|
);
|
||
|
|
assert_eq!(trace.total_latency_ms, 42);
|
||
|
|
assert_eq!(trace.candidates[0].id, "cand-2");
|
||
|
|
}
|