riglr_core/
lib.rs

1//! # riglr-core
2//!
3//! Chain-agnostic foundation for the riglr ecosystem, providing core abstractions
4//! for multi-blockchain tool orchestration without SDK dependencies.
5//!
6//! ## Design Philosophy
7//!
8//! riglr-core maintains strict chain-agnosticism through:
9//! - **Type Erasure**: Blockchain clients stored as `Arc<dyn Any>`
10//! - **Serialization Boundaries**: Transactions passed as bytes/JSON
11//! - **Dependency Injection**: ApplicationContext pattern for runtime injection
12//! - **Unidirectional Flow**: Tools depend on core, never the reverse
13//!
14//! ## Architecture Overview
15//!
16//! The riglr-core crate provides three main architectural patterns:
17//!
18//! ### 1. Unified Tool Architecture with ApplicationContext
19//!
20//! All tools now use a single `ApplicationContext` parameter that provides access to RPC clients,
21//! configuration, and other shared resources. Tools are defined with the `#[tool]` macro:
22//!
23//! ```ignore
24//! use riglr_core::{Tool, ToolError, provider::ApplicationContext};
25//! use riglr_macros::tool;
26//!
27//! #[tool]
28//! async fn my_tool(
29//!     param1: String,
30//!     param2: u64,
31//!     context: &ApplicationContext,
32//! ) -> Result<serde_json::Value, ToolError> {
33//!     // Access RPC client from context
34//!     // In practice, use concrete types from blockchain SDKs:
35//!     // let rpc_client = context.get_extension::<Arc<MyRpcClient>>()
36//!     //     .ok_or_else(|| ToolError::permanent_string("RPC client not available"))?;
37//!
38//!     // Access configuration
39//!     let config = &context.config;
40//!
41//!     // Perform operations...
42//!     Ok(serde_json::json!({ "result": "success" }))
43//! }
44//! ```
45//!
46//! ### 2. `ToolWorker` Lifecycle
47//!
48//! Orchestrates tool execution with proper error handling and ApplicationContext:
49//!
50//! ```ignore
51//! use riglr_core::{ToolWorker, ExecutionConfig, provider::ApplicationContext};
52//! use riglr_core::idempotency::InMemoryIdempotencyStore;
53//! use riglr_config::Config;
54//! use std::sync::Arc;
55//!
56//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
57//! let config = Config::from_env();
58//! let context = ApplicationContext::from_config(&config);
59//!
60//! let worker = ToolWorker::<InMemoryIdempotencyStore>::new(
61//!     ExecutionConfig::default(),
62//!     context
63//! );
64//!
65//! // Execute tools with automatic retry logic
66//! // let result = worker.process_job(job).await?;
67//! # Ok(())
68//! # }
69//! ```
70//!
71//! ### 3. `ToolError` Structure and Classification
72//!
73//! The new ToolError structure provides rich error classification with context:
74//! - `Permanent { source, source_message, context }`: Non-retriable errors
75//! - `Retriable { source, source_message, context }`: Temporary issues that can be retried
76//! - `RateLimited { source, source_message, context, retry_after }`: Rate limiting with optional backoff
77//! - `InvalidInput { source, source_message, context }`: Input validation failures
78//! - `SignerContext(String)`: Signer-related errors
79//!
80//! ## Integration with rig
81//!
82//! riglr-core extends rig's agent capabilities with blockchain-specific tooling
83//! while maintaining compatibility with rig's execution model.
84//!
85//! ### Key Components
86//!
87//! - **[`SignerContext`]** - Thread-safe signer management for transactional operations
88//! - **[`UnifiedSigner`]** - Trait for blockchain transaction signing across chains
89//! - **[`ApplicationContext`]** - Dependency injection container with RPC client extensions
90//! - **[`ToolWorker`]** - Resilient tool execution engine with retry logic and timeouts
91//! - **[`JobQueue`]** - Distributed job processing with Redis backend
92//! - **[`Tool`]** - Core trait for defining executable tools with error handling
93//! - **[`Job`]** - Work unit representation with retry and idempotency support
94//! - **[`JobResult`]** - Structured results distinguishing success, retriable, and permanent failures
95//! - **[`retry_async`]** - Centralized retry logic with exponential backoff
96//!
97//! ### Quick Start Example
98//!
99//! ```ignore
100//! use riglr_core::{ToolWorker, ExecutionConfig, Job, JobResult, ToolError};
101//! use riglr_core::{idempotency::InMemoryIdempotencyStore, provider::ApplicationContext};
102//! use riglr_macros::tool;
103//! use riglr_config::Config;
104//! use std::sync::Arc;
105//!
106//! // Define a simple tool using the #[tool] macro
107//! #[tool]
108//! async fn greeting_tool(
109//!     name: Option<String>,
110//!     context: &ApplicationContext,
111//! ) -> Result<serde_json::Value, ToolError> {
112//!     let name = name.unwrap_or_else(|| "World".to_string());
113//!     Ok(serde_json::json!({
114//!         "message": format!("Hello, {}!", name),
115//!         "timestamp": chrono::Utc::now()
116//!     }))
117//! }
118//!
119//! # async fn example() -> anyhow::Result<()> {
120//! // Set up ApplicationContext
121//! let config = Config::from_env();
122//! let context = ApplicationContext::from_config(&config);
123//!
124//! // Set up worker with context
125//! let worker = ToolWorker::<InMemoryIdempotencyStore>::new(
126//!     ExecutionConfig::default(),
127//!     context
128//! );
129//!
130//! // Register your tool
131//! worker.register_tool(Arc::new(greeting_tool)).await;
132//!
133//! // Create and process a job
134//! let job = Job::new(
135//!     "greeting_tool",
136//!     &serde_json::json!({"name": "riglr"}),
137//!     3 // max retries
138//! )?;
139//!
140//! let result = worker.process_job(job).await.unwrap();
141//! println!("Result: {:?}", result);
142//! # Ok(())
143//! # }
144//! ```
145//!
146//! ### Architecture Patterns
147//!
148//! #### 1. Unified Tool Architecture
149//!
150//! All tools now use ApplicationContext for consistent access to resources:
151//!
152//! ```ignore
153//! use riglr_core::{ToolError, provider::ApplicationContext};
154//! use riglr_macros::tool;
155//! use std::sync::Arc;
156//!
157//! // READ-ONLY OPERATIONS: Access RPC client from context
158//! #[tool]
159//! async fn get_sol_balance(
160//!     address: String,
161//!     context: &ApplicationContext,
162//! ) -> Result<serde_json::Value, ToolError> {
163//!     // In practice, use concrete RPC client types from blockchain SDKs:
164//!     // let rpc_client = context.get_extension::<Arc<MyRpcClient>>()
165//!     //     .ok_or_else(|| ToolError::permanent_string("RPC client not available"))?;
166//!     
167//!     // Query balance using RPC client
168//!     // ...
169//!     Ok(serde_json::json!({ "balance_sol": 1.5 }))
170//! }
171//!
172//! // TRANSACTIONAL OPERATIONS: Use SignerContext within tools
173//! #[tool]
174//! async fn transfer_sol(
175//!     recipient: String,
176//!     amount: f64,
177//!     context: &ApplicationContext,
178//! ) -> Result<serde_json::Value, ToolError> {
179//!     // Access signer through SignerContext::current()
180//!     // Perform transaction signing and submission
181//!     // ...
182//!     Ok(serde_json::json!({
183//!         "transaction_hash": "ABC123",
184//!         "amount": amount,
185//!         "recipient": recipient
186//!     }))
187//! }
188//! ```
189//!
190//! #### 2. Resilient Tool Execution
191//!
192//! The [`ToolWorker`] provides automatic retry logic, timeouts, idempotency checking,
193//! and resource management:
194//!
195//! - **Exponential backoff** for failed operations
196//! - **Configurable timeouts** to prevent hanging operations
197//! - **Idempotency store** integration for safe retries
198//! - **Resource limits** to prevent system overload
199//! - **Comprehensive metrics** for monitoring
200//!
201//! #### 3. Error Classification
202//!
203//! The system distinguishes between different types of errors to enable intelligent retry logic:
204//!
205//! - **Retriable errors**: Network timeouts, rate limits, temporary service unavailability
206//! - **Permanent errors**: Invalid parameters, insufficient funds, authorization failures
207//! - **System errors**: Configuration issues, internal failures
208//!
209//! ### Features
210//!
211//! - `redis` - Enable Redis-backed job queue and idempotency store (default)
212//! - `tokio` - Async runtime support (required)
213//! - `tracing` - Structured logging and observability
214//!
215//! ### Production Considerations
216//!
217//! For production deployments, consider:
218//!
219//! - Setting appropriate resource limits based on your infrastructure
220//! - Configuring Redis with persistence and clustering for reliability
221//! - Implementing proper monitoring and alerting on worker metrics
222//! - Using structured logging with correlation IDs for debugging
223//! - Setting up dead letter queues for failed job analysis
224
225/// Error types and handling for riglr-core operations.
226pub mod error;
227/// Idempotency store implementations for ensuring operation uniqueness.
228pub mod idempotency;
229/// Job definition and processing structures.
230pub mod jobs;
231/// Application context and RPC provider infrastructure.
232pub mod provider;
233/// Extension traits for ergonomic client access.
234pub mod provider_extensions;
235/// Queue implementations for distributed job processing.
236pub mod queue;
237/// Retry logic with exponential backoff for resilient operations.
238pub mod retry;
239/// Sentiment analysis abstraction for news and social media content.
240pub mod sentiment;
241/// Thread-safe signer context and transaction signing abstractions.
242pub mod signer;
243/// Task spawning utilities that preserve signer context across async boundaries.
244pub mod spawn;
245/// Core tool trait and execution infrastructure.
246pub mod tool;
247/// Internal utility functions and helpers.
248pub mod util;
249
250// Re-export configuration types from riglr-config
251pub use riglr_config::{
252    AppConfig, Config, DatabaseConfig, Environment, FeaturesConfig, NetworkConfig, ProvidersConfig,
253};
254
255pub use error::{CoreError, ToolError};
256pub use idempotency::*;
257pub use jobs::*;
258pub use queue::*;
259pub use signer::SignerContext;
260pub use signer::SignerError;
261pub use signer::UnifiedSigner;
262pub use tool::*;
263// Note: util functions are for internal use only
264// Environment variable functions should not be used by library consumers
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use std::sync::Arc;
270
271    #[derive(Clone)]
272    struct MockTool {
273        name: String,
274        should_fail: bool,
275    }
276
277    #[async_trait::async_trait]
278    impl Tool for MockTool {
279        async fn execute(
280            &self,
281            params: serde_json::Value,
282            _context: &crate::provider::ApplicationContext,
283        ) -> Result<JobResult, ToolError> {
284            if self.should_fail {
285                return Err(ToolError::permanent_string("Mock tool failure"));
286            }
287
288            let message = params["message"].as_str().unwrap_or("Hello");
289            Ok(JobResult::success(&format!("{}: {}", self.name, message))?)
290        }
291
292        fn name(&self) -> &str {
293            &self.name
294        }
295
296        fn description(&self) -> &str {
297            ""
298        }
299    }
300
301    #[tokio::test]
302    async fn test_job_creation() -> anyhow::Result<()> {
303        let job = Job::new("test_tool", &serde_json::json!({"message": "test"}), 3)?;
304
305        assert_eq!(job.tool_name, "test_tool");
306        assert_eq!(job.max_retries, 3);
307        assert_eq!(job.retry_count, 0);
308        Ok(())
309    }
310
311    #[tokio::test]
312    async fn test_job_result_success() -> anyhow::Result<()> {
313        let result = JobResult::success(&"test result")?;
314
315        match result {
316            JobResult::Success { value, tx_hash } => {
317                assert_eq!(value, serde_json::json!("test result"));
318                assert!(tx_hash.is_none());
319            }
320            _ => panic!("Expected success result"),
321        }
322        Ok(())
323    }
324
325    #[tokio::test]
326    async fn test_job_result_failure() {
327        let result = JobResult::Failure {
328            error: crate::error::ToolError::retriable_string("test error"),
329        };
330
331        match result {
332            JobResult::Failure { ref error } => {
333                assert!(error.contains("test error"));
334                assert!(result.is_retriable());
335            }
336            _ => panic!("Expected failure result"),
337        }
338    }
339
340    #[tokio::test]
341    async fn test_tool_worker_creation() {
342        let _worker = ToolWorker::<InMemoryIdempotencyStore>::new(
343            ExecutionConfig::default(),
344            provider::ApplicationContext::default(),
345        );
346
347        // Verify worker was created successfully - creation itself is the test
348    }
349
350    #[tokio::test]
351    async fn test_tool_registration_and_execution() -> anyhow::Result<()> {
352        let worker = ToolWorker::<InMemoryIdempotencyStore>::new(
353            ExecutionConfig::default(),
354            provider::ApplicationContext::default(),
355        );
356
357        let tool = Arc::new(MockTool {
358            name: "test_tool".to_string(),
359            should_fail: false,
360        });
361
362        worker.register_tool(tool).await;
363
364        let job = Job::new(
365            "test_tool",
366            &serde_json::json!({"message": "Hello World"}),
367            3,
368        )?;
369
370        let result = worker.process_job(job).await;
371        assert!(result.is_ok());
372
373        match result.unwrap() {
374            JobResult::Success { value, .. } => {
375                assert!(value.as_str().unwrap().contains("Hello World"));
376            }
377            _ => panic!("Expected successful job result"),
378        }
379
380        Ok(())
381    }
382
383    #[tokio::test]
384    async fn test_tool_error_handling() -> anyhow::Result<()> {
385        let worker = ToolWorker::<InMemoryIdempotencyStore>::new(
386            ExecutionConfig::default(),
387            provider::ApplicationContext::default(),
388        );
389
390        let tool = Arc::new(MockTool {
391            name: "failing_tool".to_string(),
392            should_fail: true,
393        });
394
395        worker.register_tool(tool).await;
396
397        let job = Job::new("failing_tool", &serde_json::json!({"message": "test"}), 3)?;
398
399        let result = worker.process_job(job).await;
400        assert!(result.is_ok());
401
402        // The tool should handle errors gracefully and return a failure result
403        match result.unwrap() {
404            JobResult::Failure { error, .. } => {
405                assert!(error.contains("Mock tool failure"));
406            }
407            _ => panic!("Expected failure job result"),
408        }
409
410        Ok(())
411    }
412
413    #[test]
414    fn test_tool_error_types() {
415        let retriable = ToolError::retriable_string("Network timeout");
416        assert!(retriable.is_retriable());
417        assert!(!retriable.is_rate_limited());
418
419        let rate_limited = ToolError::rate_limited_string("API rate limit exceeded");
420        assert!(rate_limited.is_retriable());
421        assert!(rate_limited.is_rate_limited());
422
423        let permanent = ToolError::permanent_string("Invalid parameters");
424        assert!(!permanent.is_retriable());
425        assert!(!permanent.is_rate_limited());
426    }
427
428    #[test]
429    fn test_error_conversions() {
430        let anyhow_error = anyhow::anyhow!("Test error");
431        let tool_error: ToolError = ToolError::permanent_string(anyhow_error.to_string());
432        assert!(!tool_error.is_retriable());
433
434        let string_error = "Test string error".to_string();
435        let tool_error: ToolError = ToolError::permanent_string(string_error);
436        assert!(!tool_error.is_retriable());
437
438        let str_error = "Test str error";
439        let tool_error: ToolError = ToolError::permanent_string(str_error);
440        assert!(!tool_error.is_retriable());
441    }
442
443    #[tokio::test]
444    async fn test_execution_config() {
445        let config = ExecutionConfig::default();
446
447        assert!(config.default_timeout > std::time::Duration::from_millis(0));
448        assert!(config.max_concurrency > 0);
449        assert!(config.initial_retry_delay > std::time::Duration::from_millis(0));
450    }
451
452    #[tokio::test]
453    async fn test_idempotency_store() -> anyhow::Result<()> {
454        let store = InMemoryIdempotencyStore::new();
455        let key = "test_key";
456        let value = serde_json::json!({"test": "value"});
457
458        // Check key doesn't exist initially
459        assert!(store.get(key).await?.is_none());
460
461        // Store a value
462        let job_result = JobResult::success(&value)?;
463        store
464            .set(
465                key,
466                Arc::new(job_result),
467                std::time::Duration::from_secs(60),
468            )
469            .await?;
470
471        // Retrieve the value
472        let retrieved = store.get(key).await?;
473        assert!(retrieved.is_some());
474        // Verify the stored result matches what we expect
475        if let Some(arc_result) = retrieved {
476            if let JobResult::Success { value, .. } = arc_result.as_ref() {
477                assert_eq!(*value, serde_json::json!({"test": "value"}));
478            } else {
479                panic!("Expected Success variant");
480            }
481        }
482
483        Ok(())
484    }
485
486    // Additional comprehensive tests for 100% coverage
487
488    #[test]
489    fn test_job_result_success_with_tx_hash() -> anyhow::Result<()> {
490        let result = JobResult::success_with_tx(&"test result", "0x123abc")?;
491
492        match result {
493            JobResult::Success { value, tx_hash } => {
494                assert_eq!(value, serde_json::json!("test result"));
495                assert_eq!(tx_hash, Some("0x123abc".to_string()));
496            }
497            _ => panic!("Expected success result with tx hash"),
498        }
499        Ok(())
500    }
501
502    #[test]
503    fn test_job_result_permanent_failure() {
504        let result = JobResult::Failure {
505            error: crate::error::ToolError::permanent_string("invalid parameters"),
506        };
507
508        match result {
509            JobResult::Failure { ref error } => {
510                assert!(error.contains("invalid parameters"));
511                assert!(!result.is_retriable());
512            }
513            _ => panic!("Expected permanent failure result"),
514        }
515    }
516
517    #[test]
518    fn test_job_retry_logic() -> anyhow::Result<()> {
519        let mut job = Job::new("test_tool", &serde_json::json!({"test": "data"}), 3)?;
520
521        assert!(job.can_retry());
522        assert_eq!(job.retry_count, 0);
523
524        // Simulate retries
525        job.retry_count = 1;
526        assert!(job.can_retry());
527
528        job.retry_count = 2;
529        assert!(job.can_retry());
530
531        job.retry_count = 3;
532        assert!(!job.can_retry());
533
534        job.retry_count = 4;
535        assert!(!job.can_retry());
536
537        Ok(())
538    }
539
540    #[test]
541    fn test_job_new_idempotent() -> anyhow::Result<()> {
542        let job = Job::new_idempotent(
543            "test_tool",
544            &serde_json::json!({"message": "test"}),
545            5,
546            "unique_key_123",
547        )?;
548
549        assert_eq!(job.tool_name, "test_tool");
550        assert_eq!(job.max_retries, 5);
551        assert_eq!(job.retry_count, 0);
552        assert_eq!(job.idempotency_key, Some("unique_key_123".to_string()));
553        Ok(())
554    }
555
556    #[test]
557    fn test_job_new_idempotent_without_key() -> anyhow::Result<()> {
558        let job = Job::new("test_tool", &serde_json::json!({"message": "test"}), 2)?;
559
560        assert_eq!(job.tool_name, "test_tool");
561        assert_eq!(job.max_retries, 2);
562        assert_eq!(job.retry_count, 0);
563        assert!(job.idempotency_key.is_none());
564        Ok(())
565    }
566
567    #[test]
568    fn test_core_error_display() {
569        let queue_error = CoreError::Queue("Failed to connect".to_string());
570        assert!(queue_error
571            .to_string()
572            .contains("Queue error: Failed to connect"));
573
574        let job_error = CoreError::JobExecution("Timeout occurred".to_string());
575        assert!(job_error
576            .to_string()
577            .contains("Job execution error: Timeout occurred"));
578
579        let generic_error = CoreError::Generic("Something went wrong".to_string());
580        assert!(generic_error
581            .to_string()
582            .contains("Core error: Something went wrong"));
583    }
584
585    #[test]
586    fn test_core_error_from_serde_json() {
587        let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
588        let core_error: CoreError = json_error.into();
589
590        match core_error {
591            CoreError::Serialization(_) => (),
592            _ => panic!("Expected Serialization error"),
593        }
594    }
595
596    #[test]
597    fn test_tool_error_permanent() {
598        let error = ToolError::permanent_string("Authorization failed");
599        assert!(!error.is_retriable());
600        assert!(!error.is_rate_limited());
601        assert!(error.to_string().contains("Authorization failed"));
602    }
603
604    #[test]
605    fn test_tool_error_retriable() {
606        let error = ToolError::retriable_string("Connection timeout");
607        assert!(error.is_retriable());
608        assert!(!error.is_rate_limited());
609        assert!(error.to_string().contains("Connection timeout"));
610    }
611
612    #[test]
613    fn test_tool_error_rate_limited() {
614        let error = ToolError::rate_limited_string("Too many requests");
615        assert!(error.is_retriable());
616        assert!(error.is_rate_limited());
617        assert!(error.to_string().contains("Too many requests"));
618    }
619
620    #[test]
621    fn test_tool_error_from_anyhow() {
622        let anyhow_error = anyhow::anyhow!("Test anyhow error");
623        let tool_error: ToolError = ToolError::permanent_string(anyhow_error.to_string());
624        assert!(!tool_error.is_retriable());
625        assert!(!tool_error.is_rate_limited());
626    }
627
628    #[test]
629    fn test_tool_error_from_str() {
630        let str_error = "Test str error";
631        let tool_error: ToolError = ToolError::permanent_string(str_error);
632        assert!(!tool_error.is_retriable());
633        assert!(!tool_error.is_rate_limited());
634        assert!(tool_error.to_string().contains("Test str error"));
635    }
636
637    #[test]
638    fn test_tool_error_from_string() {
639        let string_error = "Test string error".to_string();
640        let tool_error: ToolError = ToolError::permanent_string(string_error);
641        assert!(!tool_error.is_retriable());
642        assert!(!tool_error.is_rate_limited());
643        assert!(tool_error.to_string().contains("Test string error"));
644    }
645
646    #[test]
647    fn test_execution_config_customization() {
648        let mut config = ExecutionConfig::default();
649
650        // Test default values exist
651        assert!(config.default_timeout > std::time::Duration::from_millis(0));
652        assert!(config.max_concurrency > 0);
653        assert!(config.initial_retry_delay > std::time::Duration::from_millis(0));
654
655        // Test that we can modify the config
656        config.max_concurrency = 10;
657        config.default_timeout = std::time::Duration::from_secs(30);
658        config.initial_retry_delay = std::time::Duration::from_millis(100);
659
660        assert_eq!(config.max_concurrency, 10);
661        assert_eq!(config.default_timeout, std::time::Duration::from_secs(30));
662        assert_eq!(
663            config.initial_retry_delay,
664            std::time::Duration::from_millis(100)
665        );
666    }
667
668    #[tokio::test]
669    async fn test_tool_worker_with_non_existent_tool() -> anyhow::Result<()> {
670        let worker = ToolWorker::<InMemoryIdempotencyStore>::new(
671            ExecutionConfig::default(),
672            provider::ApplicationContext::default(),
673        );
674
675        let job = Job::new(
676            "non_existent_tool",
677            &serde_json::json!({"message": "test"}),
678            1,
679        )?;
680
681        let result = worker.process_job(job).await;
682
683        // Should return error for missing tool
684        assert!(result.is_err());
685
686        let error = result.unwrap_err();
687        assert!(error.to_string().contains("not found") || error.to_string().contains("Tool"));
688
689        Ok(())
690    }
691
692    #[tokio::test]
693    async fn test_job_with_empty_parameters() -> anyhow::Result<()> {
694        let job = Job::new("test_tool", &serde_json::json!({}), 1)?;
695
696        assert_eq!(job.tool_name, "test_tool");
697        assert_eq!(job.params, serde_json::json!({}));
698        assert_eq!(job.max_retries, 1);
699        assert_eq!(job.retry_count, 0);
700        Ok(())
701    }
702
703    #[tokio::test]
704    async fn test_job_with_null_parameters() -> anyhow::Result<()> {
705        let job = Job::new("test_tool", &serde_json::Value::Null, 1)?;
706
707        assert_eq!(job.tool_name, "test_tool");
708        assert_eq!(job.params, serde_json::Value::Null);
709        assert_eq!(job.max_retries, 1);
710        assert_eq!(job.retry_count, 0);
711        Ok(())
712    }
713
714    #[test]
715    fn test_job_retry_boundary_conditions() -> anyhow::Result<()> {
716        // Test with max_retries = 0
717        let job = Job::new("test_tool", &serde_json::json!({}), 0)?;
718        assert!(!job.can_retry());
719
720        // Test with max_retries = 1
721        let mut job = Job::new("test_tool", &serde_json::json!({}), 1)?;
722        assert!(job.can_retry());
723        job.retry_count = 1;
724        assert!(!job.can_retry());
725
726        Ok(())
727    }
728
729    #[tokio::test]
730    async fn test_mock_tool_with_empty_message() -> anyhow::Result<()> {
731        let tool = MockTool {
732            name: "test_tool".to_string(),
733            should_fail: false,
734        };
735
736        let context = provider::ApplicationContext::default();
737        let result = tool.execute(serde_json::json!({}), &context).await.unwrap();
738
739        match result {
740            JobResult::Success { value, .. } => {
741                assert!(value.as_str().unwrap().contains("test_tool"));
742                assert!(value.as_str().unwrap().contains("Hello")); // Default message
743            }
744            _ => panic!("Expected success result"),
745        }
746
747        Ok(())
748    }
749
750    #[test]
751    fn test_mock_tool_name_and_description() {
752        let tool = MockTool {
753            name: "custom_tool".to_string(),
754            should_fail: false,
755        };
756
757        assert_eq!(tool.name(), "custom_tool");
758        assert_eq!(tool.description(), "");
759    }
760
761    #[tokio::test]
762    async fn test_worker_multiple_tool_registration() -> anyhow::Result<()> {
763        let worker = ToolWorker::<InMemoryIdempotencyStore>::new(
764            ExecutionConfig::default(),
765            provider::ApplicationContext::default(),
766        );
767
768        let tool1 = Arc::new(MockTool {
769            name: "tool1".to_string(),
770            should_fail: false,
771        });
772
773        let tool2 = Arc::new(MockTool {
774            name: "tool2".to_string(),
775            should_fail: false,
776        });
777
778        worker.register_tool(tool1).await;
779        worker.register_tool(tool2).await;
780
781        // Test both tools work
782        let job1 = Job::new("tool1", &serde_json::json!({"message": "test1"}), 1)?;
783        let result1 = worker.process_job(job1).await?;
784
785        let job2 = Job::new("tool2", &serde_json::json!({"message": "test2"}), 1)?;
786        let result2 = worker.process_job(job2).await?;
787
788        match (&result1, &result2) {
789            (JobResult::Success { .. }, JobResult::Success { .. }) => (),
790            _ => panic!("Both tools should succeed"),
791        }
792
793        Ok(())
794    }
795
796    #[test]
797    fn test_job_result_serialization() -> anyhow::Result<()> {
798        let success_result = JobResult::success(&"test data")?;
799        let serialized = serde_json::to_string(&success_result)?;
800        let deserialized: JobResult = serde_json::from_str(&serialized)?;
801
802        match deserialized {
803            JobResult::Success { value, tx_hash } => {
804                assert_eq!(value, serde_json::json!("test data"));
805                assert!(tx_hash.is_none());
806            }
807            _ => panic!("Expected success result after deserialization"),
808        }
809
810        Ok(())
811    }
812
813    #[test]
814    fn test_job_result_failure_serialization() -> anyhow::Result<()> {
815        let failure_result = JobResult::Failure {
816            error: crate::error::ToolError::retriable_string("network error"),
817        };
818        let serialized = serde_json::to_string(&failure_result)?;
819        let deserialized: JobResult = serde_json::from_str(&serialized)?;
820
821        match deserialized {
822            JobResult::Failure { ref error } => {
823                assert!(error.contains("network error"));
824                assert!(deserialized.is_retriable());
825            }
826            _ => panic!("Expected failure result after deserialization"),
827        }
828
829        Ok(())
830    }
831
832    #[tokio::test]
833    async fn test_idempotency_store_expiration() -> anyhow::Result<()> {
834        let store = InMemoryIdempotencyStore::new();
835        let key = "expiring_key";
836        let value = serde_json::json!({"test": "expiry"});
837
838        let job_result = JobResult::success(&value)?;
839
840        // Set with very short expiration
841        store
842            .set(
843                key,
844                Arc::new(job_result),
845                std::time::Duration::from_millis(1),
846            )
847            .await?;
848
849        // Wait for expiration
850        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
851
852        // Key should be expired/removed
853        let retrieved = store.get(key).await?;
854        assert!(retrieved.is_none());
855
856        Ok(())
857    }
858}