pub mod error;
pub mod idempotency;
pub mod jobs;
pub mod provider;
pub mod provider_extensions;
pub mod queue;
pub mod retry;
pub mod sentiment;
pub mod signer;
pub mod spawn;
pub mod tool;
pub mod util;
pub use riglr_config::{
AppConfig, Config, DatabaseConfig, Environment, FeaturesConfig, NetworkConfig, ProvidersConfig,
};
pub use error::{CoreError, ToolError};
pub use idempotency::*;
pub use jobs::*;
pub use queue::*;
pub use signer::SignerContext;
pub use signer::SignerError;
pub use signer::UnifiedSigner;
pub use tool::*;
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Arc;
#[derive(Clone)]
struct MockTool {
name: String,
should_fail: bool,
}
#[async_trait::async_trait]
impl Tool for MockTool {
async fn execute(
&self,
params: serde_json::Value,
_context: &crate::provider::ApplicationContext,
) -> Result<JobResult, ToolError> {
if self.should_fail {
return Err(ToolError::permanent_string("Mock tool failure"));
}
let message = params["message"].as_str().unwrap_or("Hello");
Ok(JobResult::success(&format!("{}: {}", self.name, message))?)
}
fn name(&self) -> &str {
&self.name
}
fn description(&self) -> &str {
""
}
}
#[tokio::test]
async fn test_job_creation() -> anyhow::Result<()> {
let job = Job::new("test_tool", &serde_json::json!({"message": "test"}), 3)?;
assert_eq!(job.tool_name, "test_tool");
assert_eq!(job.max_retries, 3);
assert_eq!(job.retry_count, 0);
Ok(())
}
#[tokio::test]
async fn test_job_result_success() -> anyhow::Result<()> {
let result = JobResult::success(&"test result")?;
match result {
JobResult::Success { value, tx_hash } => {
assert_eq!(value, serde_json::json!("test result"));
assert!(tx_hash.is_none());
}
_ => panic!("Expected success result"),
}
Ok(())
}
#[tokio::test]
async fn test_job_result_failure() {
let result = JobResult::Failure {
error: crate::error::ToolError::retriable_string("test error"),
};
match result {
JobResult::Failure { ref error } => {
assert!(error.contains("test error"));
assert!(result.is_retriable());
}
_ => panic!("Expected failure result"),
}
}
#[tokio::test]
async fn test_tool_worker_creation() {
let _worker = ToolWorker::<InMemoryIdempotencyStore>::new(
ExecutionConfig::default(),
provider::ApplicationContext::default(),
);
}
#[tokio::test]
async fn test_tool_registration_and_execution() -> anyhow::Result<()> {
let worker = ToolWorker::<InMemoryIdempotencyStore>::new(
ExecutionConfig::default(),
provider::ApplicationContext::default(),
);
let tool = Arc::new(MockTool {
name: "test_tool".to_string(),
should_fail: false,
});
worker.register_tool(tool).await;
let job = Job::new(
"test_tool",
&serde_json::json!({"message": "Hello World"}),
3,
)?;
let result = worker.process_job(job).await;
assert!(result.is_ok());
match result.unwrap() {
JobResult::Success { value, .. } => {
assert!(value.as_str().unwrap().contains("Hello World"));
}
_ => panic!("Expected successful job result"),
}
Ok(())
}
#[tokio::test]
async fn test_tool_error_handling() -> anyhow::Result<()> {
let worker = ToolWorker::<InMemoryIdempotencyStore>::new(
ExecutionConfig::default(),
provider::ApplicationContext::default(),
);
let tool = Arc::new(MockTool {
name: "failing_tool".to_string(),
should_fail: true,
});
worker.register_tool(tool).await;
let job = Job::new("failing_tool", &serde_json::json!({"message": "test"}), 3)?;
let result = worker.process_job(job).await;
assert!(result.is_ok());
match result.unwrap() {
JobResult::Failure { error, .. } => {
assert!(error.contains("Mock tool failure"));
}
_ => panic!("Expected failure job result"),
}
Ok(())
}
#[test]
fn test_tool_error_types() {
let retriable = ToolError::retriable_string("Network timeout");
assert!(retriable.is_retriable());
assert!(!retriable.is_rate_limited());
let rate_limited = ToolError::rate_limited_string("API rate limit exceeded");
assert!(rate_limited.is_retriable());
assert!(rate_limited.is_rate_limited());
let permanent = ToolError::permanent_string("Invalid parameters");
assert!(!permanent.is_retriable());
assert!(!permanent.is_rate_limited());
}
#[test]
fn test_error_conversions() {
let anyhow_error = anyhow::anyhow!("Test error");
let tool_error: ToolError = ToolError::permanent_string(anyhow_error.to_string());
assert!(!tool_error.is_retriable());
let string_error = "Test string error".to_string();
let tool_error: ToolError = ToolError::permanent_string(string_error);
assert!(!tool_error.is_retriable());
let str_error = "Test str error";
let tool_error: ToolError = ToolError::permanent_string(str_error);
assert!(!tool_error.is_retriable());
}
#[tokio::test]
async fn test_execution_config() {
let config = ExecutionConfig::default();
assert!(config.default_timeout > std::time::Duration::from_millis(0));
assert!(config.max_concurrency > 0);
assert!(config.initial_retry_delay > std::time::Duration::from_millis(0));
}
#[tokio::test]
async fn test_idempotency_store() -> anyhow::Result<()> {
let store = InMemoryIdempotencyStore::new();
let key = "test_key";
let value = serde_json::json!({"test": "value"});
assert!(store.get(key).await?.is_none());
let job_result = JobResult::success(&value)?;
store
.set(
key,
Arc::new(job_result),
std::time::Duration::from_secs(60),
)
.await?;
let retrieved = store.get(key).await?;
assert!(retrieved.is_some());
if let Some(arc_result) = retrieved {
if let JobResult::Success { value, .. } = arc_result.as_ref() {
assert_eq!(*value, serde_json::json!({"test": "value"}));
} else {
panic!("Expected Success variant");
}
}
Ok(())
}
#[test]
fn test_job_result_success_with_tx_hash() -> anyhow::Result<()> {
let result = JobResult::success_with_tx(&"test result", "0x123abc")?;
match result {
JobResult::Success { value, tx_hash } => {
assert_eq!(value, serde_json::json!("test result"));
assert_eq!(tx_hash, Some("0x123abc".to_string()));
}
_ => panic!("Expected success result with tx hash"),
}
Ok(())
}
#[test]
fn test_job_result_permanent_failure() {
let result = JobResult::Failure {
error: crate::error::ToolError::permanent_string("invalid parameters"),
};
match result {
JobResult::Failure { ref error } => {
assert!(error.contains("invalid parameters"));
assert!(!result.is_retriable());
}
_ => panic!("Expected permanent failure result"),
}
}
#[test]
fn test_job_retry_logic() -> anyhow::Result<()> {
let mut job = Job::new("test_tool", &serde_json::json!({"test": "data"}), 3)?;
assert!(job.can_retry());
assert_eq!(job.retry_count, 0);
job.retry_count = 1;
assert!(job.can_retry());
job.retry_count = 2;
assert!(job.can_retry());
job.retry_count = 3;
assert!(!job.can_retry());
job.retry_count = 4;
assert!(!job.can_retry());
Ok(())
}
#[test]
fn test_job_new_idempotent() -> anyhow::Result<()> {
let job = Job::new_idempotent(
"test_tool",
&serde_json::json!({"message": "test"}),
5,
"unique_key_123",
)?;
assert_eq!(job.tool_name, "test_tool");
assert_eq!(job.max_retries, 5);
assert_eq!(job.retry_count, 0);
assert_eq!(job.idempotency_key, Some("unique_key_123".to_string()));
Ok(())
}
#[test]
fn test_job_new_idempotent_without_key() -> anyhow::Result<()> {
let job = Job::new("test_tool", &serde_json::json!({"message": "test"}), 2)?;
assert_eq!(job.tool_name, "test_tool");
assert_eq!(job.max_retries, 2);
assert_eq!(job.retry_count, 0);
assert!(job.idempotency_key.is_none());
Ok(())
}
#[test]
fn test_core_error_display() {
let queue_error = CoreError::Queue("Failed to connect".to_string());
assert!(queue_error
.to_string()
.contains("Queue error: Failed to connect"));
let job_error = CoreError::JobExecution("Timeout occurred".to_string());
assert!(job_error
.to_string()
.contains("Job execution error: Timeout occurred"));
let generic_error = CoreError::Generic("Something went wrong".to_string());
assert!(generic_error
.to_string()
.contains("Core error: Something went wrong"));
}
#[test]
fn test_core_error_from_serde_json() {
let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
let core_error: CoreError = json_error.into();
match core_error {
CoreError::Serialization(_) => (),
_ => panic!("Expected Serialization error"),
}
}
#[test]
fn test_tool_error_permanent() {
let error = ToolError::permanent_string("Authorization failed");
assert!(!error.is_retriable());
assert!(!error.is_rate_limited());
assert!(error.to_string().contains("Authorization failed"));
}
#[test]
fn test_tool_error_retriable() {
let error = ToolError::retriable_string("Connection timeout");
assert!(error.is_retriable());
assert!(!error.is_rate_limited());
assert!(error.to_string().contains("Connection timeout"));
}
#[test]
fn test_tool_error_rate_limited() {
let error = ToolError::rate_limited_string("Too many requests");
assert!(error.is_retriable());
assert!(error.is_rate_limited());
assert!(error.to_string().contains("Too many requests"));
}
#[test]
fn test_tool_error_from_anyhow() {
let anyhow_error = anyhow::anyhow!("Test anyhow error");
let tool_error: ToolError = ToolError::permanent_string(anyhow_error.to_string());
assert!(!tool_error.is_retriable());
assert!(!tool_error.is_rate_limited());
}
#[test]
fn test_tool_error_from_str() {
let str_error = "Test str error";
let tool_error: ToolError = ToolError::permanent_string(str_error);
assert!(!tool_error.is_retriable());
assert!(!tool_error.is_rate_limited());
assert!(tool_error.to_string().contains("Test str error"));
}
#[test]
fn test_tool_error_from_string() {
let string_error = "Test string error".to_string();
let tool_error: ToolError = ToolError::permanent_string(string_error);
assert!(!tool_error.is_retriable());
assert!(!tool_error.is_rate_limited());
assert!(tool_error.to_string().contains("Test string error"));
}
#[test]
fn test_execution_config_customization() {
let mut config = ExecutionConfig::default();
assert!(config.default_timeout > std::time::Duration::from_millis(0));
assert!(config.max_concurrency > 0);
assert!(config.initial_retry_delay > std::time::Duration::from_millis(0));
config.max_concurrency = 10;
config.default_timeout = std::time::Duration::from_secs(30);
config.initial_retry_delay = std::time::Duration::from_millis(100);
assert_eq!(config.max_concurrency, 10);
assert_eq!(config.default_timeout, std::time::Duration::from_secs(30));
assert_eq!(
config.initial_retry_delay,
std::time::Duration::from_millis(100)
);
}
#[tokio::test]
async fn test_tool_worker_with_non_existent_tool() -> anyhow::Result<()> {
let worker = ToolWorker::<InMemoryIdempotencyStore>::new(
ExecutionConfig::default(),
provider::ApplicationContext::default(),
);
let job = Job::new(
"non_existent_tool",
&serde_json::json!({"message": "test"}),
1,
)?;
let result = worker.process_job(job).await;
assert!(result.is_err());
let error = result.unwrap_err();
assert!(error.to_string().contains("not found") || error.to_string().contains("Tool"));
Ok(())
}
#[tokio::test]
async fn test_job_with_empty_parameters() -> anyhow::Result<()> {
let job = Job::new("test_tool", &serde_json::json!({}), 1)?;
assert_eq!(job.tool_name, "test_tool");
assert_eq!(job.params, serde_json::json!({}));
assert_eq!(job.max_retries, 1);
assert_eq!(job.retry_count, 0);
Ok(())
}
#[tokio::test]
async fn test_job_with_null_parameters() -> anyhow::Result<()> {
let job = Job::new("test_tool", &serde_json::Value::Null, 1)?;
assert_eq!(job.tool_name, "test_tool");
assert_eq!(job.params, serde_json::Value::Null);
assert_eq!(job.max_retries, 1);
assert_eq!(job.retry_count, 0);
Ok(())
}
#[test]
fn test_job_retry_boundary_conditions() -> anyhow::Result<()> {
let job = Job::new("test_tool", &serde_json::json!({}), 0)?;
assert!(!job.can_retry());
let mut job = Job::new("test_tool", &serde_json::json!({}), 1)?;
assert!(job.can_retry());
job.retry_count = 1;
assert!(!job.can_retry());
Ok(())
}
#[tokio::test]
async fn test_mock_tool_with_empty_message() -> anyhow::Result<()> {
let tool = MockTool {
name: "test_tool".to_string(),
should_fail: false,
};
let context = provider::ApplicationContext::default();
let result = tool.execute(serde_json::json!({}), &context).await.unwrap();
match result {
JobResult::Success { value, .. } => {
assert!(value.as_str().unwrap().contains("test_tool"));
assert!(value.as_str().unwrap().contains("Hello")); }
_ => panic!("Expected success result"),
}
Ok(())
}
#[test]
fn test_mock_tool_name_and_description() {
let tool = MockTool {
name: "custom_tool".to_string(),
should_fail: false,
};
assert_eq!(tool.name(), "custom_tool");
assert_eq!(tool.description(), "");
}
#[tokio::test]
async fn test_worker_multiple_tool_registration() -> anyhow::Result<()> {
let worker = ToolWorker::<InMemoryIdempotencyStore>::new(
ExecutionConfig::default(),
provider::ApplicationContext::default(),
);
let tool1 = Arc::new(MockTool {
name: "tool1".to_string(),
should_fail: false,
});
let tool2 = Arc::new(MockTool {
name: "tool2".to_string(),
should_fail: false,
});
worker.register_tool(tool1).await;
worker.register_tool(tool2).await;
let job1 = Job::new("tool1", &serde_json::json!({"message": "test1"}), 1)?;
let result1 = worker.process_job(job1).await?;
let job2 = Job::new("tool2", &serde_json::json!({"message": "test2"}), 1)?;
let result2 = worker.process_job(job2).await?;
match (&result1, &result2) {
(JobResult::Success { .. }, JobResult::Success { .. }) => (),
_ => panic!("Both tools should succeed"),
}
Ok(())
}
#[test]
fn test_job_result_serialization() -> anyhow::Result<()> {
let success_result = JobResult::success(&"test data")?;
let serialized = serde_json::to_string(&success_result)?;
let deserialized: JobResult = serde_json::from_str(&serialized)?;
match deserialized {
JobResult::Success { value, tx_hash } => {
assert_eq!(value, serde_json::json!("test data"));
assert!(tx_hash.is_none());
}
_ => panic!("Expected success result after deserialization"),
}
Ok(())
}
#[test]
fn test_job_result_failure_serialization() -> anyhow::Result<()> {
let failure_result = JobResult::Failure {
error: crate::error::ToolError::retriable_string("network error"),
};
let serialized = serde_json::to_string(&failure_result)?;
let deserialized: JobResult = serde_json::from_str(&serialized)?;
match deserialized {
JobResult::Failure { ref error } => {
assert!(error.contains("network error"));
assert!(deserialized.is_retriable());
}
_ => panic!("Expected failure result after deserialization"),
}
Ok(())
}
#[tokio::test]
async fn test_idempotency_store_expiration() -> anyhow::Result<()> {
let store = InMemoryIdempotencyStore::new();
let key = "expiring_key";
let value = serde_json::json!({"test": "expiry"});
let job_result = JobResult::success(&value)?;
store
.set(
key,
Arc::new(job_result),
std::time::Duration::from_millis(1),
)
.await?;
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
let retrieved = store.get(key).await?;
assert!(retrieved.is_none());
Ok(())
}
}