#![allow(clippy::unwrap_used)] use std::sync::Arc; use axum::{ body::Body, http::{Method, Request, StatusCode}, }; use tidal_server::{router::build_router, state::ServerState}; use tidaldb::TidalDb; use tower::ServiceExt; const AS_OF: &str = "2000000000000000000"; fn make_app() -> axum::Router { let (schema, profiles) = tidal_server::config::load_schema(None).unwrap(); let db = TidalDb::builder() .ephemeral() .with_schema(schema) .with_profiles(profiles) .open() .unwrap(); build_router( Arc::new(ServerState::new(db)), Arc::new(tidal_server::cluster::security::ClusterCreds::unauthenticated()), ) } async fn post_rank(body: serde_json::Value) -> (StatusCode, serde_json::Value) { let response = make_app() .oneshot( Request::builder() .method(Method::POST) .uri("/rank") .header("Content-Type", "application/json") .body(Body::from(serde_json::to_vec(&body).unwrap())) .unwrap(), ) .await .unwrap(); let status = response.status(); let bytes = axum::body::to_bytes(response.into_body(), usize::MAX) .await .unwrap(); let body = serde_json::from_slice(&bytes).unwrap_or(serde_json::Value::Null); (status, body) } fn rank_body(candidates: &serde_json::Value) -> serde_json::Value { serde_json::json!({ "profile": "contest_qualified_hot", "profile_version": 1, "as_of_nanos": AS_OF, "candidates": candidates, }) } #[tokio::test] async fn rank_wire_contract_is_lossless_complete_and_deterministic() { let (status, body) = post_rank(rank_body(&serde_json::json!([ { "entity_id": 1, "created_at_nanos": "1999913600000000000", "signals": { "hearts": 8.0, "comments": 4.0, "exposure": 100.0 } }, { "entity_id": 9, "created_at_nanos": "1999913600000000000", "signals": { "hearts": 8.0, "comments": 4.0, "exposure": 100_000.0 } } ]))) .await; assert_eq!(status, StatusCode::OK, "body: {body}"); assert_eq!(body["profile"], "contest_qualified_hot"); assert_eq!(body["profile_version"], 1); assert_eq!(body["as_of_nanos"], AS_OF); let items = body["items"].as_array().unwrap(); assert_eq!(items.len(), 2); assert_eq!(items[0]["entity_id"], 9); assert_eq!(items[0]["rank"], 1); assert_eq!(items[1]["entity_id"], 1); assert_eq!(items[1]["rank"], 2); assert_eq!(items[0]["score"], items[1]["score"]); assert_ne!( items[0]["components"]["response_rate"], items[1]["components"]["response_rate"] ); for item in items { for field in [ "hearts", "comments", "exposure", "engagement", "response_rate", "freshness", "age_hours", ] { assert!( item["components"].get(field).is_some(), "missing {field}: {item}" ); } } } #[tokio::test] async fn rank_requires_a_pinned_clock() { let mut request = rank_body(&serde_json::json!([])); request.as_object_mut().unwrap().remove("as_of_nanos"); assert!(post_rank(request).await.0.is_client_error()); } #[tokio::test] async fn rank_rejects_unpinned_profile_invalid_candidates_and_numeric_timestamps() { let mut wrong_version = rank_body(&serde_json::json!([])); wrong_version["profile_version"] = serde_json::json!(2); assert_eq!(post_rank(wrong_version).await.0, StatusCode::BAD_REQUEST); let duplicate = serde_json::json!([ { "entity_id": 1, "created_at_nanos": AS_OF, "signals": { "hearts": 1.0, "comments": 0.0, "exposure": 1.0 } }, { "entity_id": 1, "created_at_nanos": AS_OF, "signals": { "hearts": 2.0, "comments": 0.0, "exposure": 2.0 } } ]); assert_eq!( post_rank(rank_body(&duplicate)).await.0, StatusCode::BAD_REQUEST ); let future = serde_json::json!([ { "entity_id": 1, "created_at_nanos": "2000000000000000001", "signals": { "hearts": 1.0, "comments": 0.0, "exposure": 1.0 } } ]); assert_eq!( post_rank(rank_body(&future)).await.0, StatusCode::BAD_REQUEST ); let negative = serde_json::json!([ { "entity_id": 1, "created_at_nanos": AS_OF, "signals": { "hearts": -1.0, "comments": 0.0, "exposure": 1.0 } } ]); assert_eq!( post_rank(rank_body(&negative)).await.0, StatusCode::BAD_REQUEST ); let mut numeric = rank_body(&serde_json::json!([])); numeric["as_of_nanos"] = serde_json::json!(2_000_000_000_000_000_000u64); assert!(post_rank(numeric).await.0.is_client_error()); } #[tokio::test] async fn rank_rejects_an_oversized_set_before_per_candidate_conversion() { let candidates = (0..10_001) .map(|entity_id| { serde_json::json!({ "entity_id": entity_id, "created_at_nanos": "not-a-timestamp", "signals": { "hearts": 0.0, "comments": 0.0, "exposure": 0.0 } }) }) .collect(); let (status, body) = post_rank(rank_body(&serde_json::Value::Array(candidates))).await; assert_eq!(status, StatusCode::BAD_REQUEST); let text = body.to_string(); assert!(text.contains("at most 10000 candidates"), "{body}"); assert!(!text.contains("created_at_nanos"), "{body}"); }