mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
fix: restore security hardening compatibility and validation
Restore authorized rule reveal, explicit full HTTP capture and retention, video task business fields, and valid payment URLs. Add opt-in credential preservation for trusted recovery, fix frontend type contracts and async races, and eliminate PostgreSQL test fixture resource leaks. Document audit coverage and successful fmt and CI-scoped Clippy checks.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1374,8 +1374,8 @@ WHERE u.request_id = ANY($1)
|
||||
"#;
|
||||
const UPSERT_USAGE_ROUTING_SNAPSHOT_SQL: &str =
|
||||
include_str!("queries/upsert_usage_routing_snapshot_sql.sql");
|
||||
#[cfg(test)]
|
||||
const UPSERT_USAGE_HTTP_AUDIT_SQL: &str = include_str!("queries/upsert_usage_http_audit_sql.sql");
|
||||
const UPSERT_USAGE_BODY_BLOB_SQL: &str = include_str!("queries/upsert_usage_body_blob_sql.sql");
|
||||
const UPSERT_USAGE_SETTLEMENT_PRICING_SNAPSHOT_SQL: &str =
|
||||
include_str!("queries/upsert_usage_settlement_pricing_snapshot_sql.sql");
|
||||
|
||||
@@ -13362,19 +13362,36 @@ async fn sync_usage_body_blob_storage<'e, E>(
|
||||
executor: E,
|
||||
request_id: &str,
|
||||
field: UsageBodyField,
|
||||
_value: Option<&Value>,
|
||||
_storage: &UsageBodyStorage,
|
||||
_clear_existing: bool,
|
||||
value: Option<&Value>,
|
||||
storage: &UsageBodyStorage,
|
||||
clear_existing: bool,
|
||||
) -> Result<(), DataLayerError>
|
||||
where
|
||||
E: sqlx::Executor<'e, Database = Postgres>,
|
||||
{
|
||||
let body_ref = usage_body_ref(request_id, field);
|
||||
sqlx::query(DELETE_USAGE_BODY_BLOB_SQL)
|
||||
.bind(&body_ref)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
if clear_existing {
|
||||
sqlx::query(DELETE_USAGE_BODY_BLOB_SQL)
|
||||
.bind(&body_ref)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
} else if let Some(payload_gzip) = storage.detached_blob_bytes.as_ref() {
|
||||
sqlx::query(UPSERT_USAGE_BODY_BLOB_SQL)
|
||||
.bind(&body_ref)
|
||||
.bind(request_id)
|
||||
.bind(field.as_storage_field())
|
||||
.bind(payload_gzip)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
} else if value.is_some() {
|
||||
sqlx::query(DELETE_USAGE_BODY_BLOB_SQL)
|
||||
.bind(&body_ref)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -13383,43 +13400,46 @@ async fn sync_usage_http_audit_storage<'e, E>(
|
||||
request_id: &str,
|
||||
headers: &UsageHttpAuditHeaders<'_>,
|
||||
refs: &UsageHttpAuditRefs,
|
||||
_states: &UsageHttpAuditStates,
|
||||
states: &UsageHttpAuditStates,
|
||||
body_capture_mode: &str,
|
||||
) -> Result<(), DataLayerError>
|
||||
where
|
||||
E: sqlx::Executor<'e, Database = Postgres>,
|
||||
{
|
||||
if headers.any_present() || refs.any_present() || body_capture_mode != "none" {
|
||||
return Err(DataLayerError::InvalidInput(
|
||||
"usage HTTP capture persistence is disabled".to_string(),
|
||||
));
|
||||
if !headers.any_present()
|
||||
&& !refs.any_present()
|
||||
&& !states.any_present()
|
||||
&& body_capture_mode == "none"
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
r#"
|
||||
WITH deleted_audit AS (
|
||||
DELETE FROM usage_http_audits WHERE request_id = $1
|
||||
)
|
||||
UPDATE usage
|
||||
SET request_headers = NULL,
|
||||
request_body = NULL,
|
||||
provider_request_headers = NULL,
|
||||
provider_request_body = NULL,
|
||||
response_headers = NULL,
|
||||
response_body = NULL,
|
||||
client_response_headers = NULL,
|
||||
client_response_body = NULL,
|
||||
request_body_compressed = NULL,
|
||||
provider_request_body_compressed = NULL,
|
||||
response_body_compressed = NULL,
|
||||
client_response_body_compressed = NULL
|
||||
WHERE request_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(request_id)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
sqlx::query(UPSERT_USAGE_HTTP_AUDIT_SQL)
|
||||
.bind(request_id)
|
||||
.bind(headers.request_headers_json)
|
||||
.bind(headers.provider_request_headers_json)
|
||||
.bind(headers.response_headers_json)
|
||||
.bind(headers.client_response_headers_json)
|
||||
.bind(refs.request_body_ref.as_deref())
|
||||
.bind(refs.provider_request_body_ref.as_deref())
|
||||
.bind(refs.response_body_ref.as_deref())
|
||||
.bind(refs.client_response_body_ref.as_deref())
|
||||
.bind(usage_body_capture_state_bind_text(
|
||||
states.request_body_state,
|
||||
))
|
||||
.bind(usage_body_capture_state_bind_text(
|
||||
states.provider_request_body_state,
|
||||
))
|
||||
.bind(usage_body_capture_state_bind_text(
|
||||
states.response_body_state,
|
||||
))
|
||||
.bind(usage_body_capture_state_bind_text(
|
||||
states.client_response_body_state,
|
||||
))
|
||||
.bind(body_capture_mode)
|
||||
.execute(executor)
|
||||
.await
|
||||
.map_postgres_err()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -187,6 +187,152 @@ async fn pending_batch_is_opt_in_and_rejects_non_pending_before_connecting() {
|
||||
.contains("pending usage batch requires pending status"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires AETHER_TEST_DATABASE_URL and PostgreSQL migrations"]
|
||||
async fn live_full_http_capture_round_trips_for_direct_and_batch_writes() {
|
||||
let factory = PostgresPoolFactory::new(PostgresPoolConfig {
|
||||
database_url: std::env::var("AETHER_TEST_DATABASE_URL").unwrap(),
|
||||
min_connections: 1,
|
||||
max_connections: 2,
|
||||
acquire_timeout_ms: 10_000,
|
||||
idle_timeout_ms: 30_000,
|
||||
max_lifetime_ms: 60_000,
|
||||
statement_cache_capacity: 64,
|
||||
require_ssl: false,
|
||||
})
|
||||
.unwrap();
|
||||
let repository = SqlxUsageReadRepository::new(factory.connect_lazy().unwrap());
|
||||
crate::run_migrations(repository.pool()).await.unwrap();
|
||||
|
||||
for batch in [false, true] {
|
||||
let request_id = format!("req-full-capture-{}", uuid::Uuid::new_v4().simple());
|
||||
let now_unix_secs = Utc::now().timestamp() as u64;
|
||||
let mut pending = fast_clear_usage_record(
|
||||
&request_id,
|
||||
"full-capture-test",
|
||||
now_unix_secs,
|
||||
false,
|
||||
UsageBodyCaptureState::Inline,
|
||||
None,
|
||||
);
|
||||
pending.request_headers =
|
||||
Some(json!({"content-type": "application/json", "authorization": "Bearer private"}));
|
||||
pending.request_body =
|
||||
Some(json!({"messages": [{"role": "user", "content": "original request"}]}));
|
||||
pending.request_body_state = Some(UsageBodyCaptureState::Inline);
|
||||
pending.provider_request_body = Some(json!({"input": "provider request"}));
|
||||
pending.response_body = Some(json!("pending response"));
|
||||
pending.response_body_state = Some(UsageBodyCaptureState::Inline);
|
||||
pending.client_response_body = Some(json!("pending client response"));
|
||||
pending.client_response_body_state = Some(UsageBodyCaptureState::Inline);
|
||||
if batch {
|
||||
repository
|
||||
.upsert_pending_many(vec![pending.clone()])
|
||||
.await
|
||||
.unwrap();
|
||||
} else {
|
||||
repository.upsert(pending.clone()).await.unwrap();
|
||||
}
|
||||
for (field, expected) in [
|
||||
(UsageBodyField::RequestBody, pending.request_body.as_ref()),
|
||||
(
|
||||
UsageBodyField::ProviderRequestBody,
|
||||
pending.provider_request_body.as_ref(),
|
||||
),
|
||||
(UsageBodyField::ResponseBody, pending.response_body.as_ref()),
|
||||
(
|
||||
UsageBodyField::ClientResponseBody,
|
||||
pending.client_response_body.as_ref(),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
repository
|
||||
.resolve_body_ref(&usage_body_ref(&request_id, field))
|
||||
.await
|
||||
.unwrap()
|
||||
.as_ref(),
|
||||
expected,
|
||||
"batch={batch}, field={field:?}"
|
||||
);
|
||||
}
|
||||
|
||||
let mut terminal = fast_clear_usage_record(
|
||||
&request_id,
|
||||
"full-capture-test",
|
||||
now_unix_secs,
|
||||
true,
|
||||
UsageBodyCaptureState::None,
|
||||
None,
|
||||
);
|
||||
terminal.provider_request_body_state = None;
|
||||
terminal.response_headers =
|
||||
Some(json!({"content-type": "text/event-stream", "set-cookie": "private"}));
|
||||
terminal.response_body = Some(json!(format!(
|
||||
"data: {}\n\ndata: [DONE]\n\n",
|
||||
"streamed text".repeat(8192)
|
||||
)));
|
||||
terminal.response_body_state = Some(UsageBodyCaptureState::Inline);
|
||||
terminal.client_response_body = Some(json!({"output": "final response"}));
|
||||
terminal.client_response_body_state = Some(UsageBodyCaptureState::Inline);
|
||||
repository.upsert(terminal.clone()).await.unwrap();
|
||||
|
||||
let stored = repository
|
||||
.find_by_request_id_shallow(&request_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
stored.request_headers,
|
||||
Some(json!({"content-type": "application/json", "authorization": "[redacted]"}))
|
||||
);
|
||||
assert_eq!(
|
||||
stored.response_headers,
|
||||
Some(json!({"content-type": "text/event-stream", "set-cookie": "[redacted]"}))
|
||||
);
|
||||
for (field, expected) in [
|
||||
(UsageBodyField::RequestBody, pending.request_body.as_ref()),
|
||||
(
|
||||
UsageBodyField::ProviderRequestBody,
|
||||
pending.provider_request_body.as_ref(),
|
||||
),
|
||||
(
|
||||
UsageBodyField::ResponseBody,
|
||||
terminal.response_body.as_ref(),
|
||||
),
|
||||
(
|
||||
UsageBodyField::ClientResponseBody,
|
||||
terminal.client_response_body.as_ref(),
|
||||
),
|
||||
] {
|
||||
assert_eq!(
|
||||
stored.body_state(field),
|
||||
Some(UsageBodyCaptureState::Reference)
|
||||
);
|
||||
assert_eq!(
|
||||
stored.body_ref(field),
|
||||
Some(usage_body_ref(&request_id, field).as_str())
|
||||
);
|
||||
assert_eq!(
|
||||
repository
|
||||
.resolve_body_ref(stored.body_ref(field).unwrap())
|
||||
.await
|
||||
.unwrap()
|
||||
.as_ref(),
|
||||
expected,
|
||||
"batch={batch}, field={field:?}"
|
||||
);
|
||||
}
|
||||
let legacy_content_present: bool = sqlx::query_scalar("SELECT request_body IS NOT NULL OR request_headers IS NOT NULL OR response_body IS NOT NULL FROM usage WHERE request_id = $1")
|
||||
.bind(&request_id).fetch_one(repository.pool()).await.unwrap();
|
||||
assert!(!legacy_content_present);
|
||||
sqlx::query("DELETE FROM usage WHERE request_id = $1")
|
||||
.bind(&request_id)
|
||||
.execute(repository.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires AETHER_TEST_DATABASE_URL and PostgreSQL migrations"]
|
||||
async fn live_stale_terminal_event_is_a_full_transaction_noop() {
|
||||
@@ -253,7 +399,7 @@ async fn live_stale_terminal_event_is_a_full_transaction_noop() {
|
||||
.unwrap(),
|
||||
);
|
||||
let settlement_before = sqlx::query(
|
||||
"SELECT billing_status, billing_total_cost_usd FROM usage_settlement_snapshots WHERE request_id = $1",
|
||||
"SELECT billing_status, billing_total_cost_usd::DOUBLE PRECISION AS billing_total_cost_usd FROM usage_settlement_snapshots WHERE request_id = $1",
|
||||
)
|
||||
.bind(&request_id)
|
||||
.fetch_one(repository.pool())
|
||||
@@ -318,7 +464,7 @@ async fn live_stale_terminal_event_is_a_full_transaction_noop() {
|
||||
.unwrap(),
|
||||
);
|
||||
let settlement_after = sqlx::query(
|
||||
"SELECT billing_status, billing_total_cost_usd FROM usage_settlement_snapshots WHERE request_id = $1",
|
||||
"SELECT billing_status, billing_total_cost_usd::DOUBLE PRECISION AS billing_total_cost_usd FROM usage_settlement_snapshots WHERE request_id = $1",
|
||||
)
|
||||
.bind(&request_id)
|
||||
.fetch_one(repository.pool())
|
||||
@@ -542,7 +688,7 @@ async fn live_pending_batch_persists_auxiliary_state_and_preserves_terminal_conf
|
||||
.fetch_one(repository.pool())
|
||||
.await
|
||||
.expect("HTTP audit count should be readable");
|
||||
assert_eq!(http_count, 0);
|
||||
assert_eq!(http_count, 1);
|
||||
let blob_count = sqlx::query_scalar::<_, i64>(
|
||||
"SELECT COUNT(*)::BIGINT FROM usage_body_blobs WHERE request_id = $1",
|
||||
)
|
||||
@@ -550,7 +696,23 @@ async fn live_pending_batch_persists_auxiliary_state_and_preserves_terminal_conf
|
||||
.fetch_one(repository.pool())
|
||||
.await
|
||||
.expect("body blob count should be readable");
|
||||
assert_eq!(blob_count, 0);
|
||||
assert_eq!(blob_count, 4);
|
||||
let captured = repository
|
||||
.find_by_request_id_shallow(&rich_request_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
captured.request_headers,
|
||||
Some(json!({"x-request": "[redacted]"}))
|
||||
);
|
||||
assert_eq!(
|
||||
repository
|
||||
.resolve_body_ref(captured.body_ref(UsageBodyField::RequestBody).unwrap())
|
||||
.await
|
||||
.unwrap(),
|
||||
Some(json!({"messages": [{"role": "user", "content": "hello"}]}))
|
||||
);
|
||||
|
||||
let routing = sqlx::query(
|
||||
"SELECT candidate_id, candidate_index, selected_provider_api_key_id FROM usage_routing_snapshots WHERE request_id = $1",
|
||||
@@ -667,7 +829,7 @@ async fn live_pending_batch_and_terminal_upserts_count_each_provider_request_onc
|
||||
|
||||
let suffix = uuid::Uuid::new_v4().simple().to_string();
|
||||
let provider_name = format!("pending-terminal-race-provider-{suffix}");
|
||||
let provider_key_id = format!("pending-terminal-race-key-{suffix}");
|
||||
let provider_key_id = uuid::Uuid::new_v4().to_string();
|
||||
let now_unix_secs = Utc::now().timestamp().max(0) as u64;
|
||||
let request_ids = (0..REQUESTS)
|
||||
.map(|index| format!("req-pending-terminal-race-{index}-{suffix}"))
|
||||
@@ -793,7 +955,7 @@ async fn live_first_byte_fast_path_is_atomic_and_preserves_terminal_state() {
|
||||
let existing_request_id = format!("req-first-byte-existing-{suffix}");
|
||||
let metadata_fill_request_id = format!("req-first-byte-metadata-fill-{suffix}");
|
||||
let provider_name = format!("first-byte-fast-{suffix}");
|
||||
let missing_provider_key_id = format!("key-first-byte-missing-{suffix}");
|
||||
let missing_provider_key_id = uuid::Uuid::new_v4().to_string();
|
||||
let now_unix_secs = Utc::now().timestamp().max(0) as u64;
|
||||
|
||||
let mut missing_first_byte = first_byte_usage_record(
|
||||
@@ -1064,7 +1226,7 @@ async fn live_first_byte_reads_provider_contribution_after_waiting_for_canonical
|
||||
let suffix = uuid::Uuid::new_v4().simple().to_string();
|
||||
let request_id = format!("req-first-byte-lock-snapshot-{suffix}");
|
||||
let provider_name = format!("first-byte-lock-snapshot-{suffix}");
|
||||
let provider_key_id = format!("key-first-byte-lock-snapshot-{suffix}");
|
||||
let provider_key_id = uuid::Uuid::new_v4().to_string();
|
||||
let now_unix_secs = Utc::now().timestamp().max(0) as u64;
|
||||
let mut pending = first_byte_usage_record(
|
||||
&request_id,
|
||||
@@ -1209,14 +1371,14 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
|
||||
let request_b = format!("req-first-byte-batch-b-{suffix}");
|
||||
let request_missing = format!("req-first-byte-batch-missing-{suffix}");
|
||||
let request_terminal = format!("req-first-byte-batch-terminal-{suffix}");
|
||||
let missing_provider_key_id = format!("key-first-byte-batch-missing-{suffix}");
|
||||
let missing_provider_key_id = uuid::Uuid::new_v4().to_string();
|
||||
let now_unix_secs = Utc::now().timestamp().max(0) as u64;
|
||||
|
||||
let mut pending_a = first_byte_usage_record(
|
||||
&request_a,
|
||||
&provider_name,
|
||||
now_unix_secs,
|
||||
Some(json!({"seed": "a"})),
|
||||
Some(json!({"trace_id": "seed-a"})),
|
||||
);
|
||||
pending_a.status = "pending".to_string();
|
||||
pending_a.first_byte_time_ms = None;
|
||||
@@ -1241,7 +1403,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
|
||||
);
|
||||
terminal.is_stream = Some(true);
|
||||
terminal.first_byte_time_ms = Some(44);
|
||||
terminal.request_metadata = Some(json!({"terminal": true}));
|
||||
terminal.request_metadata = Some(json!({"trace_id": "terminal"}));
|
||||
|
||||
repository
|
||||
.upsert(pending_a)
|
||||
@@ -1260,7 +1422,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
|
||||
&request_a,
|
||||
&provider_name,
|
||||
now_unix_secs + 1,
|
||||
Some(json!({"incoming": "a"})),
|
||||
Some(json!({"trace_id": "incoming-a"})),
|
||||
);
|
||||
first_a.first_byte_time_ms = Some(30);
|
||||
first_a.has_format_conversion = None;
|
||||
@@ -1272,7 +1434,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
|
||||
&request_b,
|
||||
&provider_name,
|
||||
now_unix_secs + 1,
|
||||
Some(json!({"incoming": "b"})),
|
||||
Some(json!({"trace_id": "incoming-b"})),
|
||||
);
|
||||
first_b.has_format_conversion = Some(true);
|
||||
|
||||
@@ -1280,7 +1442,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
|
||||
&request_terminal,
|
||||
&provider_name,
|
||||
now_unix_secs + 2,
|
||||
Some(json!({"late": true})),
|
||||
Some(json!({"trace_id": "late"})),
|
||||
);
|
||||
late_terminal.first_byte_time_ms = Some(3);
|
||||
late_terminal.has_format_conversion = None;
|
||||
@@ -1288,7 +1450,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
|
||||
&request_missing,
|
||||
&provider_name,
|
||||
now_unix_secs + 1,
|
||||
Some(json!({"incoming": "missing"})),
|
||||
Some(json!({"trace_id": "incoming-missing"})),
|
||||
);
|
||||
first_missing.provider_api_key_id = Some(missing_provider_key_id.clone());
|
||||
|
||||
@@ -1345,7 +1507,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
|
||||
row_a
|
||||
.try_get::<Option<serde_json::Value>, _>("request_metadata")
|
||||
.unwrap(),
|
||||
Some(json!({"seed": "a"})),
|
||||
Some(json!({"trace_id": "seed-a"})),
|
||||
"existing metadata remains authoritative"
|
||||
);
|
||||
|
||||
@@ -1364,7 +1526,7 @@ async fn live_first_byte_batch_preserves_duplicate_order_and_terminal_guards() {
|
||||
row_b
|
||||
.try_get::<Option<serde_json::Value>, _>("request_metadata")
|
||||
.unwrap(),
|
||||
Some(json!({"incoming": "b"}))
|
||||
Some(json!({"trace_id": "incoming-b"}))
|
||||
);
|
||||
|
||||
let row_terminal = rows
|
||||
@@ -2044,7 +2206,7 @@ async fn live_provider_performance_grouping_sets_matches_separate_queries() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires AETHER_TEST_DATABASE_URL and a populated PostgreSQL database"]
|
||||
#[ignore = "requires AETHER_TEST_DATABASE_URL and PostgreSQL migrations"]
|
||||
async fn live_dashboard_daily_breakdown_uses_canonical_covering_read_path() {
|
||||
let database_url = std::env::var("AETHER_TEST_DATABASE_URL")
|
||||
.expect("AETHER_TEST_DATABASE_URL must point at the test database");
|
||||
@@ -2062,6 +2224,20 @@ async fn live_dashboard_daily_breakdown_uses_canonical_covering_read_path() {
|
||||
let repository =
|
||||
SqlxUsageReadRepository::new(factory.connect_lazy().expect("lazy pool should build"));
|
||||
let until = Utc::now().timestamp().max(0) as u64;
|
||||
crate::run_migrations(repository.pool()).await.unwrap();
|
||||
let request_id = format!("daily-breakdown-{}", uuid::Uuid::new_v4().simple());
|
||||
let provider_name = format!("daily-provider-{}", uuid::Uuid::new_v4().simple());
|
||||
repository
|
||||
.upsert(fast_clear_usage_record(
|
||||
&request_id,
|
||||
&provider_name,
|
||||
until.saturating_sub(60),
|
||||
true,
|
||||
UsageBodyCaptureState::None,
|
||||
None,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
let started = std::time::Instant::now();
|
||||
let rows = repository
|
||||
.list_dashboard_daily_breakdown(&UsageDashboardDailyBreakdownQuery {
|
||||
@@ -2077,7 +2253,17 @@ async fn live_dashboard_daily_breakdown_uses_canonical_covering_read_path() {
|
||||
started.elapsed(),
|
||||
rows.len()
|
||||
);
|
||||
assert!(!rows.is_empty());
|
||||
let seeded = rows
|
||||
.iter()
|
||||
.find(|row| row.provider == provider_name)
|
||||
.unwrap();
|
||||
assert_eq!(seeded.requests, 1);
|
||||
assert_eq!(seeded.total_tokens, 2);
|
||||
sqlx::query("DELETE FROM \"usage\" WHERE request_id = $1")
|
||||
.bind(&request_id)
|
||||
.execute(repository.pool())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -81,12 +81,12 @@ fn select_video_task_full_columns() -> String {
|
||||
|
||||
fn select_video_task_claim_columns() -> String {
|
||||
select_video_task_columns(
|
||||
"NULL::TEXT",
|
||||
"prompt",
|
||||
"NULL::jsonb",
|
||||
"NULL::INTEGER",
|
||||
"NULL::TEXT",
|
||||
"NULL::TEXT",
|
||||
"NULL::TEXT",
|
||||
"duration_seconds",
|
||||
"resolution",
|
||||
"aspect_ratio",
|
||||
"size",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1176,7 +1176,7 @@ fn map_video_task_row(row: &PgRow) -> Result<StoredVideoTask, DataLayerError> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{update_if_active_sql, upsert_sql, SqlxVideoTaskRepository};
|
||||
use super::{claim_due_sql, update_if_active_sql, upsert_sql, SqlxVideoTaskRepository};
|
||||
use crate::{PostgresPoolConfig, PostgresPoolFactory};
|
||||
use aether_data_contracts::repository::video_tasks::{
|
||||
UpsertVideoTask, VideoTaskLookupKey, VideoTaskQueryFilter, VideoTaskReadRepository,
|
||||
@@ -1240,6 +1240,142 @@ mod tests {
|
||||
assert!(update.contains("created_at = COALESCE(created_at, TO_TIMESTAMP($34))"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poll_claim_returns_business_fields_required_by_identity_guards() {
|
||||
let sql = claim_due_sql();
|
||||
for field in [
|
||||
"prompt",
|
||||
"duration_seconds",
|
||||
"resolution",
|
||||
"aspect_ratio",
|
||||
"size",
|
||||
] {
|
||||
assert!(
|
||||
sql.contains(&format!("{field} AS {field}")),
|
||||
"claim must retain {field}"
|
||||
);
|
||||
}
|
||||
assert!(sql.contains("NULL::jsonb AS original_request_body"));
|
||||
assert!(sql.contains("FOR UPDATE SKIP LOCKED"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires AETHER_TEST_DATABASE_URL and PostgreSQL migrations"]
|
||||
async fn live_video_task_capture_claim_and_completion_preserve_business_fields() {
|
||||
let database_url = std::env::var("AETHER_TEST_DATABASE_URL")
|
||||
.expect("AETHER_TEST_DATABASE_URL must point at the test database");
|
||||
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(&database_url)
|
||||
.await
|
||||
.expect("test database should connect");
|
||||
crate::run_migrations(&pool)
|
||||
.await
|
||||
.expect("test database should migrate");
|
||||
sqlx::query("CREATE TEMP TABLE video_tasks (LIKE public.video_tasks INCLUDING ALL)")
|
||||
.execute(&pool)
|
||||
.await
|
||||
.expect("isolated task table should be created");
|
||||
let repository = SqlxVideoTaskRepository::new(pool);
|
||||
for api_format in ["openai:video", "gemini:video"] {
|
||||
let task_id = uuid::Uuid::new_v4().to_string();
|
||||
let original = UpsertVideoTask {
|
||||
id: task_id.clone(),
|
||||
short_id: Some(uuid::Uuid::new_v4().simple().to_string()[..16].to_string()),
|
||||
request_id: format!("request-{task_id}"),
|
||||
user_id: None,
|
||||
api_key_id: None,
|
||||
username: Some("alice".to_string()),
|
||||
api_key_name: Some("video-client".to_string()),
|
||||
external_task_id: Some("upstream-task-1".to_string()),
|
||||
provider_id: None,
|
||||
endpoint_id: None,
|
||||
key_id: None,
|
||||
client_api_format: Some(api_format.to_string()),
|
||||
provider_api_format: Some(api_format.to_string()),
|
||||
format_converted: false,
|
||||
model: Some("video-model".to_string()),
|
||||
prompt: Some("business prompt".to_string()),
|
||||
original_request_body: Some(serde_json::json!({"token": "private"})),
|
||||
duration_seconds: Some(8),
|
||||
resolution: Some("1080p".to_string()),
|
||||
aspect_ratio: Some("16:9".to_string()),
|
||||
size: Some("1920x1080".to_string()),
|
||||
status: VideoTaskStatus::Submitted,
|
||||
progress_percent: 0,
|
||||
progress_message: None,
|
||||
retry_count: 0,
|
||||
poll_interval_seconds: 10,
|
||||
next_poll_at_unix_secs: Some(10),
|
||||
poll_count: 0,
|
||||
max_poll_count: 360,
|
||||
created_at_unix_ms: 1,
|
||||
submitted_at_unix_secs: Some(1),
|
||||
completed_at_unix_secs: None,
|
||||
updated_at_unix_secs: 1,
|
||||
error_code: None,
|
||||
error_message: None,
|
||||
video_url: None,
|
||||
request_metadata: Some(serde_json::json!({"authorization": "private"})),
|
||||
};
|
||||
let stored = repository
|
||||
.upsert(original.clone())
|
||||
.await
|
||||
.expect("task should persist");
|
||||
assert_eq!(stored.prompt, original.prompt);
|
||||
assert_eq!(stored.username, original.username);
|
||||
assert_eq!(stored.api_key_name, original.api_key_name);
|
||||
assert!(stored.original_request_body.is_none());
|
||||
assert!(stored.request_metadata.is_none());
|
||||
|
||||
let mut claimed = repository
|
||||
.claim_due(20, 50, 10)
|
||||
.await
|
||||
.expect("task should be claimed");
|
||||
assert_eq!(claimed.len(), 1);
|
||||
let mut completion: UpsertVideoTask = claimed.pop().expect("claimed task").into();
|
||||
stored
|
||||
.ensure_immutable_identity_matches(&completion)
|
||||
.expect("claim must preserve task identity");
|
||||
assert_eq!(completion.prompt, original.prompt);
|
||||
let mut mismatched = completion.clone();
|
||||
mismatched.duration_seconds = Some(99);
|
||||
assert!(repository
|
||||
.update_if_active(mismatched)
|
||||
.await
|
||||
.expect("guarded update should execute")
|
||||
.is_none());
|
||||
completion.status = VideoTaskStatus::Completed;
|
||||
completion.progress_percent = 100;
|
||||
completion.next_poll_at_unix_secs = None;
|
||||
completion.completed_at_unix_secs = Some(21);
|
||||
completion.updated_at_unix_secs = 21;
|
||||
completion.video_url = Some(
|
||||
"https://cdn.example.test/video.mp4?alt=media&signature=a%2Fb%2Bc%3D&part=2&part=1"
|
||||
.to_string(),
|
||||
);
|
||||
let completed = repository
|
||||
.update_if_active(completion.clone())
|
||||
.await
|
||||
.expect("completion should execute")
|
||||
.expect("matching active task should complete");
|
||||
assert_eq!(completed.video_url, completion.video_url);
|
||||
let reloaded = repository
|
||||
.find(VideoTaskLookupKey::Id(&task_id))
|
||||
.await
|
||||
.expect("task should reload")
|
||||
.expect("task should exist");
|
||||
assert_eq!(reloaded.status, VideoTaskStatus::Completed);
|
||||
assert_eq!(reloaded.prompt, original.prompt);
|
||||
assert_eq!(reloaded.video_url, completion.video_url);
|
||||
assert_eq!(reloaded.duration_seconds, original.duration_seconds);
|
||||
assert_eq!(reloaded.size, original.size);
|
||||
assert_eq!(reloaded.username, original.username);
|
||||
assert!(reloaded.request_metadata.is_none());
|
||||
}
|
||||
repository.pool().close().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repository_constructs_from_lazy_pool() {
|
||||
let repository = SqlxVideoTaskRepository::new(build_pool());
|
||||
|
||||
Reference in New Issue
Block a user