68 lines
2.5 KiB
Rust
68 lines
2.5 KiB
Rust
|
|
//! Verifies the cron reaper's core query/release logic by reproducing it
|
||
|
|
//! directly against the wallet primitives (the reaper itself lives in
|
||
|
|
//! apps/cron, which can't easily be imported as a lib from here -- this
|
||
|
|
//! test exercises the same "find expired holds, release them" contract
|
||
|
|
//! the cron task relies on).
|
||
|
|
|
||
|
|
use db::models::ai_credits::AiCreditsRepository;
|
||
|
|
use sqlx::PgPool;
|
||
|
|
use uuid::Uuid;
|
||
|
|
|
||
|
|
async fn pool() -> PgPool {
|
||
|
|
let url = std::env::var("TEST_DATABASE_URL")
|
||
|
|
.expect("set TEST_DATABASE_URL to run this test");
|
||
|
|
sqlx::postgres::PgPoolOptions::new()
|
||
|
|
.max_connections(5)
|
||
|
|
.connect(&url)
|
||
|
|
.await
|
||
|
|
.expect("connect to test db")
|
||
|
|
}
|
||
|
|
|
||
|
|
#[tokio::test]
|
||
|
|
async fn expired_hold_is_swept_and_credits_return_to_available() {
|
||
|
|
let pool = pool().await;
|
||
|
|
let user_id = Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap();
|
||
|
|
|
||
|
|
AiCreditsRepository::ensure_wallet(&pool, user_id).await.unwrap();
|
||
|
|
|
||
|
|
let hold = AiCreditsRepository::try_reserve_credits(&pool, user_id, "help_answer", 4, None, None)
|
||
|
|
.await
|
||
|
|
.expect("reserve");
|
||
|
|
|
||
|
|
let wallet = AiCreditsRepository::get_wallet(&pool, user_id).await.unwrap().unwrap();
|
||
|
|
assert_eq!(wallet.reserved_credits, 4, "credits should be reserved before the sweep");
|
||
|
|
|
||
|
|
// Force the hold into the past, exactly as an abandoned request would
|
||
|
|
// eventually end up (the migration default is NOW() + 5 minutes).
|
||
|
|
sqlx::query("UPDATE ai_reservation_holds SET expires_at = NOW() - INTERVAL '1 minute' WHERE id = $1")
|
||
|
|
.bind(hold.id)
|
||
|
|
.execute(&pool)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
|
||
|
|
// Reproduce the cron task's sweep query + release call.
|
||
|
|
let expired: Vec<Uuid> = sqlx::query_scalar(
|
||
|
|
"SELECT id FROM ai_reservation_holds WHERE status = 'held' AND expires_at < NOW()",
|
||
|
|
)
|
||
|
|
.fetch_all(&pool)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(expired, vec![hold.id]);
|
||
|
|
|
||
|
|
for id in expired {
|
||
|
|
let released = AiCreditsRepository::try_release_reservation(&pool, id).await.unwrap();
|
||
|
|
assert!(released);
|
||
|
|
}
|
||
|
|
|
||
|
|
let wallet = AiCreditsRepository::get_wallet(&pool, user_id).await.unwrap().unwrap();
|
||
|
|
assert_eq!(wallet.reserved_credits, 0, "reaper must release the reservation back to available");
|
||
|
|
assert_eq!(wallet.available_credits(), 10, "released credits must be fully spendable again");
|
||
|
|
|
||
|
|
let hold_row: (String,) = sqlx::query_as("SELECT status FROM ai_reservation_holds WHERE id = $1")
|
||
|
|
.bind(hold.id)
|
||
|
|
.fetch_one(&pool)
|
||
|
|
.await
|
||
|
|
.unwrap();
|
||
|
|
assert_eq!(hold_row.0, "released");
|
||
|
|
}
|