From 6dd0d5585cdeacda9f026bb66134e1bf4438003a Mon Sep 17 00:00:00 2001 From: cuteolaf Date: Thu, 8 Jan 2026 15:19:34 +0100 Subject: [PATCH 1/3] test(challenge-sdk): Add comprehensive test coverage for challenge SDK modules Note: Remaining uncovered code in platform_client.rs and server.rs HTTP handlers requires integration testing with WebSocket connections and HTTP servers, which is beyond the scope of unit tests. --- crates/challenge-sdk/src/data.rs | 185 +++++++++++ crates/challenge-sdk/src/database.rs | 161 +++++++++ crates/challenge-sdk/src/error.rs | 43 +++ crates/challenge-sdk/src/platform_client.rs | 255 ++++++++++++++ crates/challenge-sdk/src/routes.rs | 329 ++++++++++++++++++- crates/challenge-sdk/src/server.rs | 304 +++++++++++++++++ crates/challenge-sdk/src/submission_types.rs | 163 +++++++++ crates/challenge-sdk/src/test_challenge.rs | 116 +++++++ crates/challenge-sdk/src/types.rs | 80 +++++ crates/challenge-sdk/src/weight_types.rs | 23 ++ crates/challenge-sdk/src/weights.rs | 101 ++++++ 11 files changed, 1759 insertions(+), 1 deletion(-) diff --git a/crates/challenge-sdk/src/data.rs b/crates/challenge-sdk/src/data.rs index f8a16d14..7e5b51fd 100644 --- a/crates/challenge-sdk/src/data.rs +++ b/crates/challenge-sdk/src/data.rs @@ -395,6 +395,7 @@ impl Default for DataQuery { #[cfg(test)] mod tests { use super::*; + use serde_json::json; #[test] fn test_data_key_spec() { @@ -410,6 +411,37 @@ mod tests { assert_eq!(spec.ttl_blocks, 100); } + #[test] + fn test_challenge_scoped() { + let spec = DataKeySpec::new("leaderboard").challenge_scoped(); + assert_eq!(spec.scope, DataScope::Challenge); + } + + #[test] + fn test_global_scoped() { + let spec = DataKeySpec::new("global_config").global_scoped(); + assert_eq!(spec.scope, DataScope::Global); + } + + #[test] + fn test_with_schema() { + let schema = json!({"type": "number", "minimum": 0}); + let spec = DataKeySpec::new("score").with_schema(schema.clone()); + assert_eq!(spec.schema, Some(schema)); + } + + #[test] + fn test_no_consensus() { + let spec = DataKeySpec::new("local_data").no_consensus(); + assert!(!spec.requires_consensus); + } + + #[test] + fn test_min_consensus() { + let spec = DataKeySpec::new("important_data").min_consensus(5); + assert_eq!(spec.min_consensus, 5); + } + #[test] fn test_data_verification() { let accept = DataVerification::accept(); @@ -420,6 +452,35 @@ mod tests { assert_eq!(reject.reason, Some("Bad data".to_string())); } + #[test] + fn test_accept_with_transform() { + let transformed = vec![4, 5, 6]; + let verification = DataVerification::accept_with_transform(transformed.clone()); + assert!(verification.accepted); + assert_eq!(verification.transformed_value, Some(transformed)); + } + + #[test] + fn test_with_ttl() { + let verification = DataVerification::accept().with_ttl(500); + assert_eq!(verification.ttl_override, Some(500)); + } + + #[test] + fn test_with_event() { + let event = DataEvent::new("update", json!({"key": "value"})); + let verification = DataVerification::accept().with_event(event.clone()); + assert_eq!(verification.events.len(), 1); + assert_eq!(verification.events[0].event_type, "update"); + } + + #[test] + fn test_data_event_new() { + let event = DataEvent::new("test_event", json!({"data": 123})); + assert_eq!(event.event_type, "test_event"); + assert_eq!(event.data, json!({"data": 123})); + } + #[test] fn test_data_submission() { let sub = DataSubmission::new("score", vec![1, 2, 3], "validator1") @@ -430,4 +491,128 @@ mod tests { assert_eq!(sub.block_height, 100); assert_eq!(sub.epoch, 5); } + + #[test] + fn test_data_submission_with_metadata() { + let sub = DataSubmission::new("score", vec![1, 2, 3], "validator1") + .with_metadata("source", json!("test")); + + assert_eq!(sub.metadata.get("source"), Some(&json!("test"))); + } + + #[test] + fn test_value_json() { + let data = json!({"score": 85}); + let json_str = serde_json::to_vec(&data).unwrap(); + let sub = DataSubmission::new("score", json_str, "validator1"); + + let parsed: serde_json::Value = sub.value_json().unwrap(); + assert_eq!(parsed, data); + } + + #[test] + fn test_value_string() { + let text = "Hello, World!"; + let sub = DataSubmission::new("message", text.as_bytes().to_vec(), "validator1"); + + let parsed = sub.value_string().unwrap(); + assert_eq!(parsed, text); + } + + #[test] + fn test_stored_data_is_expired() { + let stored = StoredData { + key: "test".to_string(), + value: vec![1, 2, 3], + scope: DataScope::Validator, + validator: Some("validator1".to_string()), + stored_at_block: 100, + expires_at_block: Some(200), + version: 1, + }; + + assert!(!stored.is_expired(150)); + assert!(stored.is_expired(200)); + assert!(stored.is_expired(250)); + + // Test permanent storage (no expiry) + let permanent = StoredData { + expires_at_block: None, + ..stored + }; + assert!(!permanent.is_expired(1000000)); + } + + #[test] + fn test_stored_data_value_json() { + let data = json!({"result": "success"}); + let json_bytes = serde_json::to_vec(&data).unwrap(); + + let stored = StoredData { + key: "result".to_string(), + value: json_bytes, + scope: DataScope::Challenge, + validator: None, + stored_at_block: 100, + expires_at_block: None, + version: 1, + }; + + let parsed: serde_json::Value = stored.value_json().unwrap(); + assert_eq!(parsed, data); + } + + #[test] + fn test_data_query_new() { + let query = DataQuery::new(); + assert!(query.key_pattern.is_none()); + assert!(query.scope.is_none()); + assert!(query.validator.is_none()); + assert!(!query.include_expired); + assert!(query.limit.is_none()); + assert!(query.offset.is_none()); + } + + #[test] + fn test_data_query_key() { + let query = DataQuery::new().key("score*"); + assert_eq!(query.key_pattern, Some("score*".to_string())); + } + + #[test] + fn test_data_query_scope() { + let query = DataQuery::new().scope(DataScope::Challenge); + assert_eq!(query.scope, Some(DataScope::Challenge)); + } + + #[test] + fn test_data_query_validator() { + let query = DataQuery::new().validator("validator1"); + assert_eq!(query.validator, Some("validator1".to_string())); + } + + #[test] + fn test_data_query_include_expired() { + let query = DataQuery::new().include_expired(); + assert!(query.include_expired); + } + + #[test] + fn test_data_query_limit() { + let query = DataQuery::new().limit(50); + assert_eq!(query.limit, Some(50)); + } + + #[test] + fn test_data_query_offset() { + let query = DataQuery::new().offset(100); + assert_eq!(query.offset, Some(100)); + } + + #[test] + fn test_data_query_default() { + let query = DataQuery::default(); + assert!(query.key_pattern.is_none()); + assert!(!query.include_expired); + } } diff --git a/crates/challenge-sdk/src/database.rs b/crates/challenge-sdk/src/database.rs index e261a1d6..99c1f102 100644 --- a/crates/challenge-sdk/src/database.rs +++ b/crates/challenge-sdk/src/database.rs @@ -294,6 +294,15 @@ mod tests { assert!(db.is_ok()); } + #[test] + fn test_challenge_id() { + let dir = tempdir().unwrap(); + let challenge_id = ChallengeId::new(); + let db = ChallengeDatabase::open(dir.path(), challenge_id).unwrap(); + + assert_eq!(db.challenge_id(), challenge_id); + } + #[test] fn test_agent_storage() { let dir = tempdir().unwrap(); @@ -307,6 +316,21 @@ mod tests { assert_eq!(loaded.unwrap().hash, "test_hash_123"); } + #[test] + fn test_list_agents() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + let agent1 = AgentInfo::new("hash1".to_string()); + let agent2 = AgentInfo::new("hash2".to_string()); + + db.save_agent(&agent1).unwrap(); + db.save_agent(&agent2).unwrap(); + + let agents = db.list_agents().unwrap(); + assert_eq!(agents.len(), 2); + } + #[test] fn test_result_storage() { let dir = tempdir().unwrap(); @@ -321,6 +345,40 @@ mod tests { assert_eq!(results[0].score, 0.85); } + #[test] + fn test_get_all_results() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + let result1 = EvaluationResult::new(uuid::Uuid::new_v4(), "agent1".to_string(), 0.85); + let result2 = EvaluationResult::new(uuid::Uuid::new_v4(), "agent2".to_string(), 0.90); + + db.save_result(&result1).unwrap(); + db.save_result(&result2).unwrap(); + + let results = db.get_all_results().unwrap(); + assert_eq!(results.len(), 2); + } + + #[test] + fn test_get_latest_results() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + // Save multiple results for same agent + let mut result1 = EvaluationResult::new(uuid::Uuid::new_v4(), "agent1".to_string(), 0.70); + result1.timestamp = chrono::Utc::now() - chrono::Duration::hours(1); + + let result2 = EvaluationResult::new(uuid::Uuid::new_v4(), "agent1".to_string(), 0.90); + + db.save_result(&result1).unwrap(); + db.save_result(&result2).unwrap(); + + let latest = db.get_latest_results().unwrap(); + assert_eq!(latest.len(), 1); + assert_eq!(latest[0].score, 0.90); + } + #[test] fn test_kv_store() { let dir = tempdir().unwrap(); @@ -331,4 +389,107 @@ mod tests { let value: Option = db.kv_get("my_key").unwrap(); assert_eq!(value, Some(42)); } + + #[test] + fn test_kv_delete() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + db.kv_set("key_to_delete", &"value").unwrap(); + + let deleted = db.kv_delete("key_to_delete").unwrap(); + assert!(deleted); + + let value: Option = db.kv_get("key_to_delete").unwrap(); + assert!(value.is_none()); + + // Delete non-existent key + let deleted = db.kv_delete("non_existent").unwrap(); + assert!(!deleted); + } + + #[test] + fn test_kv_keys() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + db.kv_set("key1", &1).unwrap(); + db.kv_set("key2", &2).unwrap(); + db.kv_set("key3", &3).unwrap(); + + let keys = db.kv_keys().unwrap(); + assert_eq!(keys.len(), 3); + assert!(keys.contains(&"key1".to_string())); + assert!(keys.contains(&"key2".to_string())); + assert!(keys.contains(&"key3".to_string())); + } + + #[test] + fn test_set_meta() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + db.set_meta("author", "test_author").unwrap(); + + let value = db.get_meta("author").unwrap(); + assert_eq!(value, Some("test_author".to_string())); + } + + #[test] + fn test_get_meta() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + let value = db.get_meta("non_existent").unwrap(); + assert!(value.is_none()); + + db.set_meta("key", "value").unwrap(); + let value = db.get_meta("key").unwrap(); + assert_eq!(value, Some("value".to_string())); + } + + #[test] + fn test_get_version() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + // Should return 0 for new database + let version = db.get_version().unwrap(); + assert_eq!(version, 0); + } + + #[test] + fn test_set_version() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + db.set_version(5).unwrap(); + + let version = db.get_version().unwrap(); + assert_eq!(version, 5); + } + + #[test] + fn test_open_tree() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + let custom_tree = db.open_tree("custom_data").unwrap(); + + custom_tree.insert(b"key", b"value").unwrap(); + let value = custom_tree.get(b"key").unwrap(); + assert_eq!(value.as_ref().map(|v| v.as_ref()), Some(b"value".as_ref())); + } + + #[test] + fn test_flush() { + let dir = tempdir().unwrap(); + let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); + + db.kv_set("test_key", &"test_value").unwrap(); + + // Flush should succeed + let result = db.flush(); + assert!(result.is_ok()); + } } diff --git a/crates/challenge-sdk/src/error.rs b/crates/challenge-sdk/src/error.rs index 081f7e36..4b017c3b 100644 --- a/crates/challenge-sdk/src/error.rs +++ b/crates/challenge-sdk/src/error.rs @@ -90,3 +90,46 @@ impl From for ChallengeError { ChallengeError::Internal(err.to_string()) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_sled_error() { + let sled_err = sled::Error::Unsupported("test".to_string()); + let challenge_err: ChallengeError = sled_err.into(); + assert!(matches!(challenge_err, ChallengeError::Database(_))); + } + + #[test] + fn test_from_bincode_error() { + let bincode_err = bincode::Error::new(bincode::ErrorKind::Custom("test error".to_string())); + let challenge_err: ChallengeError = bincode_err.into(); + assert!(matches!(challenge_err, ChallengeError::Serialization(_))); + } + + #[test] + fn test_from_serde_json_error() { + let json_str = "{invalid json}"; + let json_err = serde_json::from_str::(json_str).unwrap_err(); + let challenge_err: ChallengeError = json_err.into(); + assert!(matches!(challenge_err, ChallengeError::Serialization(_))); + } + + #[test] + fn test_from_io_error() { + let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); + let challenge_err: ChallengeError = io_err.into(); + assert!(matches!(challenge_err, ChallengeError::Internal(_))); + } + + #[test] + fn test_error_display() { + let err = ChallengeError::Database("connection failed".to_string()); + assert_eq!(err.to_string(), "Database error: connection failed"); + + let err = ChallengeError::Evaluation("failed to evaluate".to_string()); + assert_eq!(err.to_string(), "Evaluation error: failed to evaluate"); + } +} diff --git a/crates/challenge-sdk/src/platform_client.rs b/crates/challenge-sdk/src/platform_client.rs index 5b3cadc5..fa31ad4a 100644 --- a/crates/challenge-sdk/src/platform_client.rs +++ b/crates/challenge-sdk/src/platform_client.rs @@ -454,3 +454,258 @@ pub async fn run_as_client( let mut client = PlatformClient::new(config, challenge); client.run().await } + +#[cfg(test)] +mod tests { + use super::*; + use crate::server::{ServerChallenge, ValidationRequest, ValidationResponse}; + use async_trait::async_trait; + use serde_json::json; + + // Mock challenge for testing + struct TestChallenge; + + #[async_trait] + impl ServerChallenge for TestChallenge { + fn challenge_id(&self) -> &str { + "test-challenge" + } + + fn name(&self) -> &str { + "Test Challenge" + } + + fn version(&self) -> &str { + "1.0.0" + } + + async fn evaluate( + &self, + request: EvaluationRequest, + ) -> Result { + Ok(EvaluationResponse::success( + &request.request_id, + 0.5, + json!({}), + )) + } + } + + #[test] + fn test_connection_state_variants() { + // Test that all connection states are different + assert_ne!(ConnectionState::Disconnected, ConnectionState::Connecting); + assert_ne!(ConnectionState::Connecting, ConnectionState::Connected); + assert_ne!(ConnectionState::Connected, ConnectionState::Reconnecting); + assert_ne!( + ConnectionState::Authenticating, + ConnectionState::Disconnected + ); + } + + #[test] + fn test_platform_client_config_new() { + let config = PlatformClientConfig { + url: "ws://localhost:8000/ws".to_string(), + challenge_id: "test-challenge".to_string(), + auth_token: "test-token".to_string(), + instance_id: Some("instance-1".to_string()), + reconnect_delay: Duration::from_secs(5), + max_reconnect_attempts: 10, + }; + + assert_eq!(config.url, "ws://localhost:8000/ws"); + assert_eq!(config.challenge_id, "test-challenge"); + assert_eq!(config.auth_token, "test-token"); + assert_eq!(config.instance_id, Some("instance-1".to_string())); + assert_eq!(config.reconnect_delay, Duration::from_secs(5)); + assert_eq!(config.max_reconnect_attempts, 10); + } + + #[test] + fn test_platform_client_creation() { + let config = PlatformClientConfig { + url: "ws://localhost:8000/ws".to_string(), + challenge_id: "test".to_string(), + auth_token: "token".to_string(), + instance_id: None, + reconnect_delay: Duration::from_secs(5), + max_reconnect_attempts: 10, + }; + + let challenge = TestChallenge; + let client = PlatformClient::new(config, challenge); + + // Verify client was created successfully + assert_eq!(client.config.challenge_id, "test"); + } + + #[test] + fn test_server_message_serialization() { + // Test AuthResponse + let msg = ServerMessage::AuthResponse { + success: true, + error: None, + session_id: Some("session-123".to_string()), + }; + + let json = serde_json::to_string(&msg).unwrap(); + assert!(json.contains("auth_response")); + + // Test Ping + let msg = ServerMessage::Ping { + timestamp: 1234567890, + }; + let json = serde_json::to_string(&msg).unwrap(); + assert!(json.contains("ping")); + } + + #[test] + fn test_challenge_message_serialization() { + // Test Auth message + let msg = ChallengeMessage::Auth { + challenge_id: "test".to_string(), + auth_token: "token".to_string(), + instance_id: None, + version: Some("1.0.0".to_string()), + }; + + let json = serde_json::to_string(&msg).unwrap(); + assert!(json.contains("auth")); + assert!(json.contains("test")); + + // Test Health message + let msg = ChallengeMessage::Health { + healthy: true, + load: 0.5, + pending: 2, + }; + + let json = serde_json::to_string(&msg).unwrap(); + assert!(json.contains("health")); + } + + #[test] + fn test_server_message_deserialization() { + // Test deserializing AuthResponse + let json = r#"{"type":"auth_response","data":{"success":true,"error":null,"session_id":"session-123"}}"#; + let msg: ServerMessage = serde_json::from_str(json).unwrap(); + + match msg { + ServerMessage::AuthResponse { + success, + session_id, + .. + } => { + assert!(success); + assert_eq!(session_id, Some("session-123".to_string())); + } + _ => panic!("Expected AuthResponse"), + } + } + + #[test] + fn test_challenge_message_deserialization() { + // Test deserializing Progress message + let json = r#"{"type":"progress","data":{"request_id":"req-123","progress":0.75,"message":"Processing"}}"#; + let msg: ChallengeMessage = serde_json::from_str(json).unwrap(); + + match msg { + ChallengeMessage::Progress { + request_id, + progress, + message, + } => { + assert_eq!(request_id, "req-123"); + assert_eq!(progress, 0.75); + assert_eq!(message, Some("Processing".to_string())); + } + _ => panic!("Expected Progress"), + } + } + + #[test] + fn test_server_message_evaluate() { + let eval_req = EvaluationRequest { + request_id: "req-1".to_string(), + submission_id: "sub-1".to_string(), + participant_id: "participant-1".to_string(), + data: json!({"test": "data"}), + metadata: None, + epoch: 1, + deadline: None, + }; + + let msg = ServerMessage::Evaluate(eval_req.clone()); + + // Serialize and deserialize + let json = serde_json::to_string(&msg).unwrap(); + let deserialized: ServerMessage = serde_json::from_str(&json).unwrap(); + + match deserialized { + ServerMessage::Evaluate(req) => { + assert_eq!(req.request_id, "req-1"); + assert_eq!(req.submission_id, "sub-1"); + } + _ => panic!("Expected Evaluate"), + } + } + + #[test] + fn test_challenge_message_result() { + let eval_resp = EvaluationResponse::success("req-1", 0.95, json!({"result": "ok"})); + + let msg = ChallengeMessage::Result(eval_resp.clone()); + + // Serialize and deserialize + let json = serde_json::to_string(&msg).unwrap(); + let deserialized: ChallengeMessage = serde_json::from_str(&json).unwrap(); + + match deserialized { + ChallengeMessage::Result(resp) => { + assert_eq!(resp.request_id, "req-1"); + assert_eq!(resp.score, 0.95); + assert!(resp.success); + } + _ => panic!("Expected Result"), + } + } + + #[test] + fn test_server_message_cancel() { + let msg = ServerMessage::Cancel { + request_id: "req-123".to_string(), + reason: "Timeout".to_string(), + }; + + let json = serde_json::to_string(&msg).unwrap(); + assert!(json.contains("cancel")); + assert!(json.contains("req-123")); + assert!(json.contains("Timeout")); + } + + #[test] + fn test_server_message_shutdown() { + let msg = ServerMessage::Shutdown { + reason: "Maintenance".to_string(), + restart_expected: true, + }; + + let json = serde_json::to_string(&msg).unwrap(); + assert!(json.contains("shutdown")); + assert!(json.contains("Maintenance")); + } + + #[test] + fn test_challenge_message_log() { + let msg = ChallengeMessage::Log { + level: "info".to_string(), + message: "Test log message".to_string(), + }; + + let json = serde_json::to_string(&msg).unwrap(); + assert!(json.contains("log")); + assert!(json.contains("info")); + assert!(json.contains("Test log message")); + } +} diff --git a/crates/challenge-sdk/src/routes.rs b/crates/challenge-sdk/src/routes.rs index 14028700..6ec4d7d4 100644 --- a/crates/challenge-sdk/src/routes.rs +++ b/crates/challenge-sdk/src/routes.rs @@ -522,6 +522,130 @@ impl Default for RouteBuilder { mod tests { use super::*; + #[test] + fn test_routes_manifest_new() { + let manifest = RoutesManifest::new("Test Challenge", "1.0.0"); + assert_eq!(manifest.name, "test-challenge"); // normalized + assert_eq!(manifest.version, "1.0.0"); + assert!(manifest.routes.is_empty()); + } + + #[test] + fn test_normalize_name() { + assert_eq!( + RoutesManifest::normalize_name("Test Challenge"), + "test-challenge" + ); + assert_eq!( + RoutesManifest::normalize_name("Test_Challenge"), + "test-challenge" + ); + assert_eq!( + RoutesManifest::normalize_name("Test-Challenge-123"), + "test-challenge-123" + ); + assert_eq!(RoutesManifest::normalize_name("-test-"), "test"); + // Multiple spaces get replaced with multiple dashes (no collapsing) + assert_eq!( + RoutesManifest::normalize_name("Test Challenge"), + "test--challenge" + ); + } + + #[test] + fn test_routes_manifest_with_description() { + let manifest = RoutesManifest::new("test", "1.0").with_description("A test challenge"); + assert_eq!(manifest.description, "A test challenge"); + } + + #[test] + fn test_routes_manifest_add_route() { + let route = ChallengeRoute::get("/test", "Test route"); + let manifest = RoutesManifest::new("test", "1.0").add_route(route.clone()); + + assert_eq!(manifest.routes.len(), 1); + assert_eq!(manifest.routes[0].path, "/test"); + } + + #[test] + fn test_routes_manifest_with_routes() { + let routes = vec![ + ChallengeRoute::get("/route1", "Route 1"), + ChallengeRoute::post("/route2", "Route 2"), + ]; + + let manifest = RoutesManifest::new("test", "1.0").with_routes(routes); + assert_eq!(manifest.routes.len(), 2); + } + + #[test] + fn test_routes_manifest_with_metadata() { + let manifest = RoutesManifest::new("test", "1.0") + .with_metadata("author", serde_json::json!("John Doe")) + .with_metadata("license", serde_json::json!("MIT")); + + assert_eq!(manifest.metadata.len(), 2); + assert_eq!( + manifest.metadata.get("author"), + Some(&serde_json::json!("John Doe")) + ); + } + + #[test] + fn test_routes_manifest_with_standard_routes() { + let manifest = RoutesManifest::new("test", "1.0").with_standard_routes(); + + assert!(manifest.routes.len() >= 6); + assert!(manifest.routes.iter().any(|r| r.path == "/submit")); + assert!(manifest.routes.iter().any(|r| r.path == "/leaderboard")); + assert!(manifest.routes.iter().any(|r| r.path == "/health")); + } + + #[test] + fn test_http_method_display() { + assert_eq!(format!("{}", HttpMethod::Get), "GET"); + assert_eq!(format!("{}", HttpMethod::Post), "POST"); + assert_eq!(format!("{}", HttpMethod::Put), "PUT"); + assert_eq!(format!("{}", HttpMethod::Delete), "DELETE"); + assert_eq!(format!("{}", HttpMethod::Patch), "PATCH"); + } + + #[test] + fn test_http_method_as_str() { + assert_eq!(HttpMethod::Get.as_str(), "GET"); + assert_eq!(HttpMethod::Post.as_str(), "POST"); + assert_eq!(HttpMethod::Put.as_str(), "PUT"); + assert_eq!(HttpMethod::Delete.as_str(), "DELETE"); + assert_eq!(HttpMethod::Patch.as_str(), "PATCH"); + } + + #[test] + fn test_challenge_route_put() { + let route = ChallengeRoute::put("/update", "Update resource"); + assert_eq!(route.method, HttpMethod::Put); + assert_eq!(route.path, "/update"); + assert_eq!(route.description, "Update resource"); + } + + #[test] + fn test_challenge_route_delete() { + let route = ChallengeRoute::delete("/remove", "Remove resource"); + assert_eq!(route.method, HttpMethod::Delete); + assert_eq!(route.path, "/remove"); + } + + #[test] + fn test_challenge_route_with_auth() { + let route = ChallengeRoute::get("/private", "Private route").with_auth(); + assert!(route.requires_auth); + } + + #[test] + fn test_challenge_route_with_rate_limit() { + let route = ChallengeRoute::post("/submit", "Submit").with_rate_limit(10); + assert_eq!(route.rate_limit, 10); + } + #[test] fn test_route_matching() { let route = ChallengeRoute::get("/agent/:hash", "Get agent"); @@ -538,6 +662,176 @@ mod tests { assert!(route.matches("GET", "/user/abc123").is_none()); } + #[test] + fn test_route_request_new() { + let req = RouteRequest::new("GET", "/test"); + assert_eq!(req.method, "GET"); + assert_eq!(req.path, "/test"); + assert!(req.params.is_empty()); + assert!(req.query.is_empty()); + } + + #[test] + fn test_route_request_with_params() { + let mut params = HashMap::new(); + params.insert("id".to_string(), "123".to_string()); + + let req = RouteRequest::new("GET", "/test").with_params(params); + assert_eq!(req.param("id"), Some("123")); + } + + #[test] + fn test_route_request_with_query() { + let mut query = HashMap::new(); + query.insert("limit".to_string(), "10".to_string()); + + let req = RouteRequest::new("GET", "/test").with_query(query); + assert_eq!(req.query_param("limit"), Some("10")); + } + + #[test] + fn test_route_request_with_body() { + let body = serde_json::json!({"key": "value"}); + let req = RouteRequest::new("POST", "/test").with_body(body.clone()); + assert_eq!(req.body, body); + } + + #[test] + fn test_route_request_with_auth() { + let req = RouteRequest::new("GET", "/test").with_auth("hotkey123".to_string()); + assert_eq!(req.auth_hotkey, Some("hotkey123".to_string())); + } + + #[test] + fn test_route_request_param() { + let mut params = HashMap::new(); + params.insert("id".to_string(), "abc".to_string()); + + let req = RouteRequest::new("GET", "/test").with_params(params); + assert_eq!(req.param("id"), Some("abc")); + assert_eq!(req.param("missing"), None); + } + + #[test] + fn test_route_request_query_param() { + let mut query = HashMap::new(); + query.insert("page".to_string(), "2".to_string()); + + let req = RouteRequest::new("GET", "/test").with_query(query); + assert_eq!(req.query_param("page"), Some("2")); + assert_eq!(req.query_param("missing"), None); + } + + #[test] + fn test_route_request_parse_body() { + #[derive(serde::Deserialize)] + struct TestData { + value: i32, + } + + let body = serde_json::json!({"value": 42}); + let req = RouteRequest::new("POST", "/test").with_body(body); + + let parsed: TestData = req.parse_body().unwrap(); + assert_eq!(parsed.value, 42); + } + + #[test] + fn test_route_response_ok() { + let resp = RouteResponse::ok(serde_json::json!({"status": "ok"})); + assert_eq!(resp.status, 200); + assert!(resp.is_success()); + } + + #[test] + fn test_route_response_created() { + let resp = RouteResponse::created(serde_json::json!({"id": "123"})); + assert_eq!(resp.status, 201); + assert!(resp.is_success()); + } + + #[test] + fn test_route_response_no_content() { + let resp = RouteResponse::no_content(); + assert_eq!(resp.status, 204); + assert!(resp.is_success()); + } + + #[test] + fn test_route_response_unauthorized() { + let resp = RouteResponse::unauthorized(); + assert_eq!(resp.status, 401); + assert!(!resp.is_success()); + } + + #[test] + fn test_route_response_forbidden() { + let resp = RouteResponse::forbidden("Access denied"); + assert_eq!(resp.status, 403); + assert!(!resp.is_success()); + } + + #[test] + fn test_route_response_rate_limited() { + let resp = RouteResponse::rate_limited(); + assert_eq!(resp.status, 429); + assert!(!resp.is_success()); + } + + #[test] + fn test_route_response_internal_error() { + let resp = RouteResponse::internal_error("Something went wrong"); + assert_eq!(resp.status, 500); + assert!(!resp.is_success()); + } + + #[test] + fn test_route_response_with_header() { + let resp = RouteResponse::ok(serde_json::json!({})).with_header("X-Custom", "value"); + + assert_eq!(resp.headers.get("X-Custom"), Some(&"value".to_string())); + } + + #[test] + fn test_route_response_is_success() { + assert!(RouteResponse::ok(serde_json::json!({})).is_success()); + assert!(RouteResponse::created(serde_json::json!({})).is_success()); + assert!(!RouteResponse::bad_request("error").is_success()); + assert!(!RouteResponse::not_found().is_success()); + } + + #[test] + fn test_route_registry_register_all() { + let mut registry = RouteRegistry::new(); + let routes = vec![ + ChallengeRoute::get("/a", "Route A"), + ChallengeRoute::post("/b", "Route B"), + ]; + + registry.register_all(routes); + assert_eq!(registry.routes().len(), 2); + } + + #[test] + fn test_route_registry_routes() { + let mut registry = RouteRegistry::new(); + registry.register(ChallengeRoute::get("/test", "Test")); + + let routes = registry.routes(); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].path, "/test"); + } + + #[test] + fn test_route_registry_is_empty() { + let registry = RouteRegistry::new(); + assert!(registry.is_empty()); + + let mut registry = RouteRegistry::new(); + registry.register(ChallengeRoute::get("/test", "Test")); + assert!(!registry.is_empty()); + } + #[test] fn test_route_builder() { let routes = RouteBuilder::new() @@ -550,7 +844,40 @@ mod tests { } #[test] - fn test_route_registry() { + fn test_route_builder_default() { + let builder = RouteBuilder::default(); + let routes = builder.build(); + assert!(routes.is_empty()); + } + + #[test] + fn test_route_builder_put() { + let routes = RouteBuilder::new() + .put("/update/:id", "Update item") + .build(); + + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].method, HttpMethod::Put); + } + + #[test] + fn test_route_builder_delete() { + let routes = RouteBuilder::new() + .delete("/remove/:id", "Remove item") + .build(); + + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].method, HttpMethod::Delete); + } + + #[test] + fn test_route_registry_new() { + let registry = RouteRegistry::new(); + assert!(registry.routes.is_empty()); + } + + #[test] + fn test_route_registry_register() { let mut registry = RouteRegistry::new(); registry.register(ChallengeRoute::get("/test", "Test")); registry.register(ChallengeRoute::get("/user/:id", "Get user")); diff --git a/crates/challenge-sdk/src/server.rs b/crates/challenge-sdk/src/server.rs index ca1c1b6f..ee29e272 100644 --- a/crates/challenge-sdk/src/server.rs +++ b/crates/challenge-sdk/src/server.rs @@ -516,3 +516,307 @@ macro_rules! impl_server_challenge { } }; } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_server_config_default() { + let config = ServerConfig::default(); + assert_eq!(config.host, "0.0.0.0"); + assert_eq!(config.port, 8080); + assert_eq!(config.max_concurrent, 4); + assert_eq!(config.timeout_secs, 600); + assert!(config.cors_enabled); + } + + #[test] + fn test_server_config_from_env() { + // Should use defaults when env vars not set + let config = ServerConfig::from_env(); + assert_eq!(config.host, "0.0.0.0"); + assert_eq!(config.port, 8080); + } + + #[test] + fn test_evaluation_request() { + let req = EvaluationRequest { + request_id: "req-123".to_string(), + submission_id: "sub-456".to_string(), + participant_id: "participant-789".to_string(), + data: json!({"code": "fn main() {}"}), + metadata: Some(json!({"version": "1.0"})), + epoch: 5, + deadline: Some(1234567890), + }; + + assert_eq!(req.request_id, "req-123"); + assert_eq!(req.epoch, 5); + assert!(req.metadata.is_some()); + } + + #[test] + fn test_evaluation_response_success() { + let resp = EvaluationResponse::success("req-123", 0.95, json!({"passed": 19, "total": 20})); + + assert!(resp.success); + assert_eq!(resp.request_id, "req-123"); + assert_eq!(resp.score, 0.95); + assert!(resp.error.is_none()); + } + + #[test] + fn test_evaluation_response_error() { + let resp = EvaluationResponse::error("req-456", "Timeout occurred"); + + assert!(!resp.success); + assert_eq!(resp.request_id, "req-456"); + assert_eq!(resp.score, 0.0); + assert_eq!(resp.error, Some("Timeout occurred".to_string())); + } + + #[test] + fn test_evaluation_response_with_time() { + let resp = EvaluationResponse::success("req", 0.8, json!({})).with_time(1500); + + assert_eq!(resp.execution_time_ms, 1500); + } + + #[test] + fn test_evaluation_response_with_cost() { + let resp = EvaluationResponse::success("req", 0.8, json!({})).with_cost(0.05); + + assert_eq!(resp.cost, Some(0.05)); + } + + #[test] + fn test_validation_request() { + let req = ValidationRequest { + data: json!({"input": "test"}), + }; + + assert_eq!(req.data["input"], "test"); + } + + #[test] + fn test_validation_response() { + let resp = ValidationResponse { + valid: true, + errors: vec![], + warnings: vec!["Consider updating format".to_string()], + }; + + assert!(resp.valid); + assert!(resp.errors.is_empty()); + assert_eq!(resp.warnings.len(), 1); + } + + #[test] + fn test_health_response() { + let health = HealthResponse { + healthy: true, + load: 0.5, + pending: 2, + uptime_secs: 3600, + version: "1.0.0".to_string(), + challenge_id: "test-challenge".to_string(), + }; + + assert!(health.healthy); + assert_eq!(health.load, 0.5); + assert_eq!(health.uptime_secs, 3600); + } + + #[test] + fn test_config_response() { + let config = ConfigResponse { + challenge_id: "test".to_string(), + name: "Test Challenge".to_string(), + version: "1.0.0".to_string(), + config_schema: Some(json!({"type": "object"})), + features: vec!["feature1".to_string(), "feature2".to_string()], + limits: ConfigLimits { + max_submission_size: Some(1024 * 1024), + max_evaluation_time: Some(300), + max_cost: Some(0.1), + }, + }; + + assert_eq!(config.challenge_id, "test"); + assert_eq!(config.features.len(), 2); + assert_eq!(config.limits.max_submission_size, Some(1024 * 1024)); + } + + #[test] + fn test_config_limits_default() { + let limits = ConfigLimits::default(); + assert!(limits.max_submission_size.is_none()); + assert!(limits.max_evaluation_time.is_none()); + assert!(limits.max_cost.is_none()); + } + + // Test with a mock challenge + struct MockChallenge; + + #[async_trait::async_trait] + impl ServerChallenge for MockChallenge { + fn challenge_id(&self) -> &str { + "mock-challenge" + } + + fn name(&self) -> &str { + "Mock Challenge" + } + + fn version(&self) -> &str { + "1.0.0" + } + + async fn evaluate( + &self, + request: EvaluationRequest, + ) -> Result { + Ok(EvaluationResponse::success( + &request.request_id, + 0.75, + json!({"mock": true}), + )) + } + } + + #[tokio::test] + async fn test_mock_challenge_evaluate() { + let challenge = MockChallenge; + + let req = EvaluationRequest { + request_id: "test".to_string(), + submission_id: "sub".to_string(), + participant_id: "participant".to_string(), + data: json!({}), + metadata: None, + epoch: 1, + deadline: None, + }; + + let result = challenge.evaluate(req).await.unwrap(); + assert!(result.success); + assert_eq!(result.score, 0.75); + } + + #[tokio::test] + async fn test_mock_challenge_validate_default() { + let challenge = MockChallenge; + + let req = ValidationRequest { data: json!({}) }; + + let result = challenge.validate(req).await.unwrap(); + assert!(result.valid); // Default implementation accepts everything + } + + #[test] + fn test_mock_challenge_config_default() { + let challenge = MockChallenge; + let config = challenge.config(); + + assert_eq!(config.challenge_id, "mock-challenge"); + assert_eq!(config.name, "Mock Challenge"); + assert_eq!(config.version, "1.0.0"); + assert!(config.features.is_empty()); + } + + #[test] + fn test_server_builder_new() { + let challenge = MockChallenge; + let builder = ChallengeServerBuilder::new(challenge); + + assert_eq!(builder.config.port, 8080); // default port + } + + #[test] + fn test_server_builder_config() { + let challenge = MockChallenge; + let custom_config = ServerConfig { + host: "127.0.0.1".to_string(), + port: 9000, + max_concurrent: 10, + timeout_secs: 120, + cors_enabled: false, + }; + + let builder = ChallengeServerBuilder::new(challenge).config(custom_config); + + assert_eq!(builder.config.host, "127.0.0.1"); + assert_eq!(builder.config.port, 9000); + } + + #[test] + fn test_server_builder_host() { + let challenge = MockChallenge; + let builder = ChallengeServerBuilder::new(challenge).host("192.168.1.1"); + + assert_eq!(builder.config.host, "192.168.1.1"); + } + + #[test] + fn test_server_builder_port() { + let challenge = MockChallenge; + let builder = ChallengeServerBuilder::new(challenge).port(3000); + + assert_eq!(builder.config.port, 3000); + } + + #[test] + fn test_server_builder_build() { + let challenge = MockChallenge; + let server = ChallengeServerBuilder::new(challenge) + .host("localhost") + .port(8888) + .build(); + + assert_eq!(server.address(), "localhost:8888"); + } + + #[test] + fn test_challenge_server_builder() { + let challenge = MockChallenge; + let builder = ChallengeServer::builder(challenge); + + assert_eq!(builder.config.port, 8080); + } + + #[test] + fn test_server_address() { + let challenge = MockChallenge; + let server = ChallengeServer::builder(challenge) + .host("0.0.0.0") + .port(8080) + .build(); + + assert_eq!(server.address(), "0.0.0.0:8080"); + } + + #[test] + fn test_server_builder_from_env() { + // Test from_env method (will use defaults when env vars not set) + let challenge = MockChallenge; + let builder = ChallengeServerBuilder::new(challenge).from_env(); + + // Should use default values from ServerConfig::from_env() + assert_eq!(builder.config.host, "0.0.0.0"); + assert_eq!(builder.config.port, 8080); + } + + #[test] + fn test_server_config_from_env_with_env_vars() { + // Note: In real usage, this would read from environment variables + // For this test, we just verify the function exists and returns defaults + let config = ServerConfig::from_env(); + + // Should return valid config (with defaults when env vars not set) + assert!(!config.host.is_empty()); + assert!(config.port > 0); + assert!(config.max_concurrent > 0); + } +} diff --git a/crates/challenge-sdk/src/submission_types.rs b/crates/challenge-sdk/src/submission_types.rs index d925267f..1640aeb0 100644 --- a/crates/challenge-sdk/src/submission_types.rs +++ b/crates/challenge-sdk/src/submission_types.rs @@ -369,4 +369,167 @@ mod tests { let different_hash = EncryptedSubmission::compute_content_hash(different_data); assert_ne!(content_hash, different_hash); } + + #[test] + fn test_compute_signature_message() { + let content_hash: [u8; 32] = [1; 32]; + let hotkey = "test_hotkey"; + let epoch = 42u64; + + let msg = EncryptedSubmission::compute_signature_message(&content_hash, hotkey, epoch); + + // Should contain all components + assert!(msg.len() > 32); // at least content_hash + something + assert!(msg.starts_with(&content_hash)); + + // Should be deterministic + let msg2 = EncryptedSubmission::compute_signature_message(&content_hash, hotkey, epoch); + assert_eq!(msg, msg2); + + // Different inputs should produce different messages + let msg3 = + EncryptedSubmission::compute_signature_message(&content_hash, "other_hotkey", epoch); + assert_ne!(msg, msg3); + } + + #[test] + fn test_hash_hex() { + let key = generate_key(); + let nonce = generate_nonce(); + let key_hash = hash_key(&key); + let data = b"test"; + let content_hash = EncryptedSubmission::compute_content_hash(data); + let encrypted = encrypt_data(data, &key, &nonce).unwrap(); + + let submission = EncryptedSubmission::new( + "challenge-1".to_string(), + "miner".to_string(), + "coldkey".to_string(), + encrypted, + key_hash, + nonce, + content_hash, + vec![], + 1, + ); + + let hex = submission.hash_hex(); + assert_eq!(hex.len(), 64); // 32 bytes = 64 hex chars + assert!(hex.chars().all(|c| c.is_ascii_hexdigit())); + } + + #[test] + fn test_content_hash_hex() { + let key = generate_key(); + let nonce = generate_nonce(); + let key_hash = hash_key(&key); + let data = b"test"; + let content_hash = EncryptedSubmission::compute_content_hash(data); + let encrypted = encrypt_data(data, &key, &nonce).unwrap(); + + let submission = EncryptedSubmission::new( + "challenge-1".to_string(), + "miner".to_string(), + "coldkey".to_string(), + encrypted, + key_hash, + nonce, + content_hash, + vec![], + 1, + ); + + let hex = submission.content_hash_hex(); + assert_eq!(hex.len(), 64); + assert!(hex.chars().all(|c| c.is_ascii_hexdigit())); + } + + #[test] + fn test_submission_ack_new() { + use platform_core::Hotkey; + + let hash: [u8; 32] = [2; 32]; + let hotkey = Hotkey::from_bytes(&[3; 32]).unwrap(); + let stake = 1000u64; + let signature = vec![4, 5, 6]; + + let ack = SubmissionAck::new(hash, hotkey.clone(), stake, signature.clone()); + + assert_eq!(ack.submission_hash, hash); + assert_eq!(ack.validator_hotkey, hotkey); + assert_eq!(ack.validator_stake, stake); + assert_eq!(ack.signature, signature); + } + + #[test] + fn test_submission_ack_hash_hex() { + use platform_core::Hotkey; + + let hash: [u8; 32] = [7; 32]; + let hotkey = Hotkey::from_bytes(&[8; 32]).unwrap(); + + let ack = SubmissionAck::new(hash, hotkey, 500, vec![]); + let hex = ack.submission_hash_hex(); + + assert_eq!(hex.len(), 64); + assert_eq!( + hex, + "0707070707070707070707070707070707070707070707070707070707070707" + ); + } + + #[test] + fn test_decryption_key_reveal_new() { + let hash: [u8; 32] = [9; 32]; + let key = vec![10, 11, 12]; + let signature = vec![13, 14, 15]; + + let reveal = DecryptionKeyReveal::new(hash, key.clone(), signature.clone()); + + assert_eq!(reveal.submission_hash, hash); + assert_eq!(reveal.decryption_key, key); + assert_eq!(reveal.miner_signature, signature); + } + + #[test] + fn test_decryption_key_reveal_verify() { + let key = generate_key(); + let key_hash = hash_key(&key); + + let reveal = DecryptionKeyReveal::new([0; 32], key.to_vec(), vec![]); + + // Should verify against correct hash + assert!(reveal.verify_key_hash(&key_hash)); + + // Should not verify against wrong hash + let wrong_hash: [u8; 32] = [255; 32]; + assert!(!reveal.verify_key_hash(&wrong_hash)); + } + + #[test] + fn test_decrypt_invalid_key_length() { + let nonce = generate_nonce(); + let data = b"test"; + let key32 = generate_key(); + let encrypted = encrypt_data(data, &key32, &nonce).unwrap(); + + // Try to decrypt with wrong key length + let short_key = vec![1, 2, 3]; // Only 3 bytes + let result = decrypt_data(&encrypted, &short_key, &nonce); + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), SubmissionError::InvalidKey)); + } + + #[test] + fn test_submission_error_variants() { + let err = SubmissionError::MinerBanned; + assert_eq!(err.to_string(), "Miner is banned"); + + let err = SubmissionError::QuorumNotReached; + assert_eq!(err.to_string(), "Quorum not reached"); + + let err = SubmissionError::DuplicateContent; + assert!(err.to_string().contains("Duplicate")); + } } diff --git a/crates/challenge-sdk/src/test_challenge.rs b/crates/challenge-sdk/src/test_challenge.rs index caf51342..12115adc 100644 --- a/crates/challenge-sdk/src/test_challenge.rs +++ b/crates/challenge-sdk/src/test_challenge.rs @@ -143,4 +143,120 @@ mod tests { let result = challenge.validate(req).await.unwrap(); assert!(!result.valid); } + + #[test] + fn test_simple_challenge_with_id() { + let challenge = SimpleTestChallenge::new("Test").with_id("custom-id"); + + assert_eq!(challenge.challenge_id(), "custom-id"); + } + + #[test] + fn test_simple_challenge_challenge_id() { + let challenge = SimpleTestChallenge::default(); + assert_eq!(challenge.challenge_id(), "simple-test-challenge"); + } + + #[test] + fn test_simple_challenge_name() { + let challenge = SimpleTestChallenge::new("My Test Challenge"); + assert_eq!(challenge.name(), "My Test Challenge"); + } + + #[test] + fn test_simple_challenge_version() { + let challenge = SimpleTestChallenge::default(); + assert_eq!(challenge.version(), "0.1.0"); + } + + #[tokio::test] + async fn test_evaluate_with_zero_bonus() { + let challenge = SimpleTestChallenge::default(); + + let req = EvaluationRequest { + request_id: "test-456".to_string(), + submission_id: "sub-456".to_string(), + participant_id: "participant-2".to_string(), + data: json!({"bonus": 0.0}), + metadata: None, + epoch: 1, + deadline: None, + }; + + let result = challenge.evaluate(req).await.unwrap(); + + assert!(result.success); + assert_eq!(result.score, 0.5); // base score only + } + + #[tokio::test] + async fn test_evaluate_with_max_bonus() { + let challenge = SimpleTestChallenge::default(); + + let req = EvaluationRequest { + request_id: "test-789".to_string(), + submission_id: "sub-789".to_string(), + participant_id: "participant-3".to_string(), + data: json!({"bonus": 0.5}), + metadata: None, + epoch: 1, + deadline: None, + }; + + let result = challenge.evaluate(req).await.unwrap(); + + assert!(result.success); + assert_eq!(result.score, 1.0); // base + max bonus + } + + #[tokio::test] + async fn test_evaluate_with_excessive_bonus() { + let challenge = SimpleTestChallenge::default(); + + let req = EvaluationRequest { + request_id: "test-999".to_string(), + submission_id: "sub-999".to_string(), + participant_id: "participant-4".to_string(), + data: json!({"bonus": 1.0}), // More than max 0.5 + metadata: None, + epoch: 1, + deadline: None, + }; + + let result = challenge.evaluate(req).await.unwrap(); + + assert!(result.success); + assert_eq!(result.score, 1.0); // Clamped to 1.0 + } + + #[tokio::test] + async fn test_evaluate_without_bonus() { + let challenge = SimpleTestChallenge::default(); + + let req = EvaluationRequest { + request_id: "test-000".to_string(), + submission_id: "sub-000".to_string(), + participant_id: "participant-5".to_string(), + data: json!({"other_field": "value"}), + metadata: None, + epoch: 1, + deadline: None, + }; + + let result = challenge.evaluate(req).await.unwrap(); + + assert!(result.success); + assert_eq!(result.score, 0.5); // base score only, no bonus field + } + + #[tokio::test] + async fn test_validate_with_null_data() { + let challenge = SimpleTestChallenge::default(); + + let req = ValidationRequest { data: json!(null) }; + + let result = challenge.validate(req).await.unwrap(); + assert!(!result.valid); + assert!(!result.errors.is_empty()); + } } diff --git a/crates/challenge-sdk/src/types.rs b/crates/challenge-sdk/src/types.rs index c6468fe2..2344beb3 100644 --- a/crates/challenge-sdk/src/types.rs +++ b/crates/challenge-sdk/src/types.rs @@ -302,6 +302,86 @@ mod tests { assert_eq!(config.max_memory_mb, 512); } + #[test] + fn test_challenge_config_with_mechanism() { + let config = ChallengeConfig::with_mechanism(5); + assert_eq!(config.mechanism_id, 5); + assert_eq!(config.evaluation_timeout_secs, 300); // other fields should use defaults + } + + #[test] + fn test_challenge_id_default() { + let id = ChallengeId::default(); + let id2 = ChallengeId::default(); + assert_ne!(id, id2); // Each default should create unique ID + } + + #[test] + fn test_challenge_id_debug() { + let id = ChallengeId::new(); + let debug_str = format!("{:?}", id); + assert!(debug_str.starts_with("Challenge(")); + assert!(debug_str.ends_with(")")); + // Length should be "Challenge(" + 8 chars + ")" = variable based on UUID + assert!(debug_str.len() >= 18); + } + + #[test] + fn test_challenge_id_display_fmt() { + let uuid = uuid::Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(); + let id = ChallengeId::from_uuid(uuid); + let display = format!("{}", id); + assert_eq!(display, "550e8400-e29b-41d4-a716-446655440000"); + } + + #[test] + fn test_agent_info_new() { + let agent = AgentInfo::new("hash123".to_string()); + assert_eq!(agent.hash, "hash123"); + assert!(agent.name.is_none()); + assert!(agent.owner.is_none()); + assert!(agent.version.is_none()); + assert_eq!(agent.metadata_json, "{}"); + } + + #[test] + fn test_agent_info_metadata() { + let mut agent = AgentInfo::new("hash".to_string()); + + // Default metadata should be null or empty object + let meta = agent.metadata(); + assert!(meta.is_object() || meta.is_null()); + + // Set metadata + let test_meta = serde_json::json!({"key": "value", "count": 42}); + agent.set_metadata(test_meta.clone()); + + let retrieved = agent.metadata(); + assert_eq!(retrieved, test_meta); + } + + #[test] + fn test_agent_info_set_metadata() { + let mut agent = AgentInfo::new("hash".to_string()); + + let meta = serde_json::json!({ + "author": "test", + "version": "1.0.0", + "tags": ["tag1", "tag2"] + }); + + agent.set_metadata(meta.clone()); + + // Verify it was serialized and stored + assert!(agent.metadata_json.contains("author")); + assert!(agent.metadata_json.contains("test")); + + // Verify we can get it back + let retrieved = agent.metadata(); + assert_eq!(retrieved["author"], "test"); + assert_eq!(retrieved["version"], "1.0.0"); + } + #[test] fn test_evaluation_job_creation() { let id = ChallengeId::new(); diff --git a/crates/challenge-sdk/src/weight_types.rs b/crates/challenge-sdk/src/weight_types.rs index e368dfc2..e798b870 100644 --- a/crates/challenge-sdk/src/weight_types.rs +++ b/crates/challenge-sdk/src/weight_types.rs @@ -147,3 +147,26 @@ impl Default for WeightConfig { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_weight_config_default() { + let config = WeightConfig::default(); + assert_eq!(config.min_validators, 3); + assert_eq!(config.min_stake_percentage, 0.3); + assert_eq!(config.outlier_zscore_threshold, 2.5); + assert_eq!(config.max_variance_threshold, 0.15); + assert_eq!(config.improvement_threshold, 0.02); + assert_eq!(config.min_score_threshold, 0.01); + } + + #[test] + fn test_weight_config_clone() { + let config = WeightConfig::default(); + let cloned = config.clone(); + assert_eq!(config.min_validators, cloned.min_validators); + } +} diff --git a/crates/challenge-sdk/src/weights.rs b/crates/challenge-sdk/src/weights.rs index 18ce269e..0551b68c 100644 --- a/crates/challenge-sdk/src/weights.rs +++ b/crates/challenge-sdk/src/weights.rs @@ -112,4 +112,105 @@ mod tests { let weights = scores_to_weights(&scores); assert!(weights.is_empty()); } + + #[test] + fn test_scores_to_weights_zero_total() { + // When total score is 0 or negative, should return empty vec + let scores = vec![("hotkey1".to_string(), 0.0), ("hotkey2".to_string(), 0.0)]; + + let weights = scores_to_weights(&scores); + assert!(weights.is_empty()); + } + + #[test] + fn test_scores_to_weights_negative_total() { + // When total score is negative (shouldn't happen but test edge case) + let scores = vec![("hotkey1".to_string(), -1.0), ("hotkey2".to_string(), -2.0)]; + + let weights = scores_to_weights(&scores); + assert!(weights.is_empty()); + } + + #[test] + fn test_create_commitment() { + let weights = vec![ + WeightAssignment::new("hotkey1".to_string(), 0.6), + WeightAssignment::new("hotkey2".to_string(), 0.4), + ]; + let secret = b"my_secret_key_123"; + + let commitment = create_commitment(&weights, secret); + + // Should be a valid hex string (64 chars for SHA256) + assert_eq!(commitment.len(), 64); + assert!(commitment.chars().all(|c| c.is_ascii_hexdigit())); + + // Same inputs should produce same commitment + let commitment2 = create_commitment(&weights, secret); + assert_eq!(commitment, commitment2); + } + + #[test] + fn test_create_commitment_different_secrets() { + let weights = vec![WeightAssignment::new("hotkey1".to_string(), 0.5)]; + + let commitment1 = create_commitment(&weights, b"secret1"); + let commitment2 = create_commitment(&weights, b"secret2"); + + // Different secrets should produce different commitments + assert_ne!(commitment1, commitment2); + } + + #[test] + fn test_create_commitment_order_independence() { + // Weights should be sorted before hashing, so order doesn't matter + let weights1 = vec![ + WeightAssignment::new("hotkey_a".to_string(), 0.5), + WeightAssignment::new("hotkey_b".to_string(), 0.5), + ]; + + let weights2 = vec![ + WeightAssignment::new("hotkey_b".to_string(), 0.5), + WeightAssignment::new("hotkey_a".to_string(), 0.5), + ]; + + let commitment1 = create_commitment(&weights1, b"secret"); + let commitment2 = create_commitment(&weights2, b"secret"); + + assert_eq!(commitment1, commitment2); + } + + #[test] + fn test_normalize_weights_zero_total() { + // When weights sum to 0, should return them unchanged + let weights = vec![ + WeightAssignment { + hotkey: "hotkey1".to_string(), + weight: 0.0, + }, + WeightAssignment { + hotkey: "hotkey2".to_string(), + weight: 0.0, + }, + ]; + + let normalized = normalize_weights(weights.clone()); + + assert_eq!(normalized.len(), 2); + assert_eq!(normalized[0].weight, 0.0); + assert_eq!(normalized[1].weight, 0.0); + } + + #[test] + fn test_normalize_weights_single() { + let weights = vec![WeightAssignment { + hotkey: "hotkey1".to_string(), + weight: 5.0, + }]; + + let normalized = normalize_weights(weights); + + assert_eq!(normalized.len(), 1); + assert!((normalized[0].weight - 1.0).abs() < 0.001); + } } From b7a7984c35664378a35801b225531b373ea638d5 Mon Sep 17 00:00:00 2001 From: cuteolaf Date: Thu, 8 Jan 2026 15:48:23 +0100 Subject: [PATCH 2/3] test(challenge-sdk): Add database persistence test across reopens Add comprehensive test to validate data durability in ChallengeDatabase by verifying that all data types persist correctly across multiple database open/close cycles. --- crates/challenge-sdk/src/database.rs | 71 ++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/crates/challenge-sdk/src/database.rs b/crates/challenge-sdk/src/database.rs index 99c1f102..3a05ed96 100644 --- a/crates/challenge-sdk/src/database.rs +++ b/crates/challenge-sdk/src/database.rs @@ -492,4 +492,75 @@ mod tests { let result = db.flush(); assert!(result.is_ok()); } + + #[test] + fn test_data_persistence_across_reopens() { + // Test that data persists after closing and reopening the database + let dir = tempdir().unwrap(); + let challenge_id = ChallengeId::new(); + + // First session: write data + { + let db = ChallengeDatabase::open(dir.path(), challenge_id).unwrap(); + + // Save an agent + let agent = AgentInfo::new("persistent_agent".to_string()); + db.save_agent(&agent).unwrap(); + + // Save a result + let result = + EvaluationResult::new(uuid::Uuid::new_v4(), "persistent_agent".to_string(), 0.95); + db.save_result(&result).unwrap(); + + // Save KV data + db.kv_set("persistent_key", &"persistent_value").unwrap(); + + // Save metadata + db.set_meta("test_meta", "meta_value").unwrap(); + db.set_version(42).unwrap(); + + // Explicitly flush to disk + db.flush().unwrap(); + + // Drop db to close it + } + + // Second session: verify data persists + { + let db = ChallengeDatabase::open(dir.path(), challenge_id).unwrap(); + + // Verify agent persists + let agent = db.get_agent("persistent_agent").unwrap(); + assert!(agent.is_some()); + assert_eq!(agent.unwrap().hash, "persistent_agent"); + + // Verify results persist + let results = db.get_results_for_agent("persistent_agent").unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].score, 0.95); + + // Verify KV data persists + let value: Option = db.kv_get("persistent_key").unwrap(); + assert_eq!(value, Some("persistent_value".to_string())); + + // Verify metadata persists + let meta = db.get_meta("test_meta").unwrap(); + assert_eq!(meta, Some("meta_value".to_string())); + + let version = db.get_version().unwrap(); + assert_eq!(version, 42); + } + + // Third session: verify data still persists (double check) + { + let db = ChallengeDatabase::open(dir.path(), challenge_id).unwrap(); + + let agents = db.list_agents().unwrap(); + assert_eq!(agents.len(), 1); + assert_eq!(agents[0].hash, "persistent_agent"); + + let all_results = db.get_all_results().unwrap(); + assert_eq!(all_results.len(), 1); + } + } } From ddf89c0788f19197ba432470efa9ba543880f9fb Mon Sep 17 00:00:00 2001 From: cuteolaf Date: Thu, 8 Jan 2026 15:58:57 +0100 Subject: [PATCH 3/3] test(challenge-sdk): enhance multi-agent testing in get_latest_results --- crates/challenge-sdk/src/database.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/challenge-sdk/src/database.rs b/crates/challenge-sdk/src/database.rs index 3a05ed96..1484c3b9 100644 --- a/crates/challenge-sdk/src/database.rs +++ b/crates/challenge-sdk/src/database.rs @@ -365,7 +365,7 @@ mod tests { let dir = tempdir().unwrap(); let db = ChallengeDatabase::open(dir.path(), ChallengeId::new()).unwrap(); - // Save multiple results for same agent + // Save multiple results for same agent (agent1) let mut result1 = EvaluationResult::new(uuid::Uuid::new_v4(), "agent1".to_string(), 0.70); result1.timestamp = chrono::Utc::now() - chrono::Duration::hours(1); @@ -374,9 +374,22 @@ mod tests { db.save_result(&result1).unwrap(); db.save_result(&result2).unwrap(); + // Add result for a different agent (agent2) + let result3 = EvaluationResult::new(uuid::Uuid::new_v4(), "agent2".to_string(), 0.80); + db.save_result(&result3).unwrap(); + let latest = db.get_latest_results().unwrap(); - assert_eq!(latest.len(), 1); - assert_eq!(latest[0].score, 0.90); + // Should have one result per agent (agent1 and agent2) + assert_eq!(latest.len(), 2); + + // Find results by agent + let agent1_result = latest.iter().find(|r| r.agent_hash == "agent1").unwrap(); + let agent2_result = latest.iter().find(|r| r.agent_hash == "agent2").unwrap(); + + // Verify agent1 has the latest score (0.90, not 0.70) + assert_eq!(agent1_result.score, 0.90); + // Verify agent2 has its only score + assert_eq!(agent2_result.score, 0.80); } #[test]