#![cfg(feature = "test-utils")]
use std::time::Duration;
use testcontainers::ImageExt as _;
use testcontainers::runners::AsyncRunner as _;
use testcontainers_modules::postgres::Postgres;
use zeph_db::DbConfig;
use zeph_db::sql;
use zeph_mcp::{ServerTrustScore, TrustScoreStore};
async fn start_pg() -> (zeph_db::DbPool, impl Drop) {
let image = Postgres::default().with_startup_timeout(Duration::from_mins(2));
let container = image.start().await.expect("docker must be available");
let host = container.get_host().await.unwrap();
let port = container.get_host_port_ipv4(5432).await.unwrap();
let url = format!("postgres://postgres:postgres@{host}:{port}/postgres");
let config = DbConfig { url, pool_size: 5 };
let pool = config.connect().await.expect("failed to connect to PG");
(pool, container)
}
#[tokio::test]
#[ignore = "requires Docker"]
async fn load_and_apply_delta_upsert_increments_counters_on_postgres() {
let (pool, _container) = start_pg().await;
let store = TrustScoreStore::new(pool);
store.init().await.unwrap();
store
.load_and_apply_delta("srv1", ServerTrustScore::SUCCESS_BOOST, 1, 0)
.await
.unwrap();
store
.load_and_apply_delta("srv1", ServerTrustScore::SUCCESS_BOOST, 1, 0)
.await
.unwrap();
let loaded = store.load("srv1").await.unwrap().unwrap();
assert_eq!(
loaded.success_count, 2,
"success_count must accumulate across two load_and_apply_delta upserts"
);
}
#[tokio::test]
#[ignore = "requires Docker"]
async fn decay_atomic_on_postgres() {
let (pool, _container) = start_pg().await;
let store = TrustScoreStore::new(pool.clone());
store.init().await.unwrap();
let old_ts = i64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
.saturating_sub(30 * 86_400),
)
.unwrap();
zeph_db::query(sql!(
"INSERT INTO mcp_trust_scores (server_id, score, success_count, failure_count, updated_at_secs)
VALUES (?, 0.9, 0, 0, ?)"
))
.bind("srv1")
.bind(old_ts)
.execute(&pool)
.await
.unwrap();
store.load_and_apply_delta("srv1", 0.0, 0, 0).await.unwrap();
let loaded = store.load("srv1").await.unwrap().unwrap();
assert!(
loaded.score < 0.9,
"score should have decayed from 0.9, got {}",
loaded.score
);
assert!(
loaded.score >= ServerTrustScore::INITIAL_SCORE,
"score should not decay below INITIAL_SCORE, got {}",
loaded.score
);
}
#[tokio::test]
#[ignore = "requires Docker"]
async fn load_all_decodes_int4_counts_on_postgres() {
let (pool, _container) = start_pg().await;
let store = TrustScoreStore::new(pool);
store.init().await.unwrap();
store
.load_and_apply_delta("srv1", 0.02, 3, 1)
.await
.unwrap();
store
.load_and_apply_delta("srv2", -0.10, 0, 2)
.await
.unwrap();
let all = store.load_all().await.unwrap();
assert_eq!(all.len(), 2, "load_all must return both servers");
let srv1 = all
.iter()
.find(|s| s.server_id == "srv1")
.expect("srv1 must be present");
assert_eq!(
srv1.success_count, 3,
"success_count must decode as the full INTEGER value, not error with ColumnDecode"
);
assert_eq!(srv1.failure_count, 1);
let srv2 = all
.iter()
.find(|s| s.server_id == "srv2")
.expect("srv2 must be present");
assert_eq!(srv2.failure_count, 2);
}