riglr_core/jobs.rs
1//! Job data structures and types for task execution.
2//!
3//! This module defines the core data structures used for representing work units
4//! and their execution results in the riglr system. Jobs are queued for execution
5//! by the [`ToolWorker`](crate::ToolWorker) and can include retry logic, idempotency
6//! keys, and structured result handling.
7//!
8//! ## Core Types
9//!
10//! - **[`Job`]** - A unit of work to be executed, with retry and idempotency support
11//! - **[`JobResult`]** - The outcome of job execution with success/failure classification
12//!
13//! ## Job Lifecycle
14//!
15//! 1. **Creation** - Jobs are created with tool name, parameters, and retry limits
16//! 2. **Queueing** - Jobs are enqueued for processing by workers
17//! 3. **Execution** - Workers execute the corresponding tool with job parameters
18//! 4. **Result** - Execution produces a [`JobResult`] indicating success or failure
19//! 5. **Retry** - Failed jobs may be retried based on failure type and retry limits
20//!
21//! ## Examples
22//!
23//! ### Creating and Processing Jobs
24//!
25//! ```rust
26//! use riglr_core::{Job, JobResult};
27//! use serde_json::json;
28//!
29//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
30//! // Create a simple job
31//! let job = Job::new(
32//! "weather_check",
33//! &json!({"city": "San Francisco"}),
34//! 3 // max retries
35//! )?;
36//!
37//! // Create an idempotent job
38//! let idempotent_job = Job::new_idempotent(
39//! "account_balance",
40//! &json!({"address": "0x123..."}),
41//! 2, // max retries
42//! "balance_check_user_123" // idempotency key
43//! )?;
44//!
45//! // Check retry status
46//! assert!(job.can_retry());
47//! assert_eq!(job.retry_count, 0);
48//! # Ok(())
49//! # }
50//! ```
51//!
52//! ### Working with Job Results
53//!
54//! ```rust
55//! use riglr_core::{JobResult, ToolError};
56//! use serde_json::json;
57//!
58//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
59//! // Successful result
60//! let success = JobResult::success(&json!({
61//! "temperature": 72,
62//! "condition": "sunny"
63//! }))?;
64//! assert!(success.is_success());
65//!
66//! // Successful transaction result
67//! let tx_result = JobResult::success_with_tx(
68//! &json!({"amount": 100, "recipient": "0xabc..."}),
69//! "0x789def..."
70//! )?;
71//!
72//! // Retriable failure using new ToolError structure
73//! let retriable = JobResult::Failure {
74//! error: ToolError::retriable_string("Network timeout")
75//! };
76//! assert!(retriable.is_retriable());
77//! assert!(!retriable.is_success());
78//!
79//! // Permanent failure using new ToolError structure
80//! let permanent = JobResult::Failure {
81//! error: ToolError::permanent_string("Invalid address format")
82//! };
83//! assert!(!permanent.is_retriable());
84//! assert!(!permanent.is_success());
85//! # Ok(())
86//! # }
87//! ```
88//!
89//! ## Error Handling Strategy
90//!
91//! The system distinguishes between two types of failures:
92//!
93//! ### Retriable Failures
94//! These represent temporary issues that may resolve on retry:
95//! - Network connectivity problems
96//! - Rate limiting from external APIs
97//! - Temporary service unavailability
98//! - Resource contention
99//!
100//! ### Permanent Failures
101//! These represent issues that won't be resolved by retrying:
102//! - Invalid parameters or malformed requests
103//! - Authentication or authorization failures
104//! - Insufficient funds or resources
105//! - Business logic violations
106//!
107//! ## Idempotency
108//!
109//! Jobs can include idempotency keys to ensure safe retry behavior:
110//!
111//! ```rust
112//! use riglr_core::Job;
113//! use serde_json::json;
114//!
115//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
116//! let job = Job::new_idempotent(
117//! "transfer",
118//! &json!({
119//! "from": "user123",
120//! "to": "user456",
121//! "amount": 100
122//! }),
123//! 3, // max retries
124//! "transfer_user123_to_user456_100_20241201" // unique key
125//! )?;
126//!
127//! // Subsequent executions with the same key return cached results
128//! # Ok(())
129//! # }
130//! ```
131
132use serde::{Deserialize, Serialize};
133use uuid::Uuid;
134
135/// A job represents a unit of work to be executed by a [`ToolWorker`](crate::ToolWorker).
136///
137/// Jobs encapsulate all the information needed to execute a tool, including
138/// the tool name, parameters, retry configuration, and optional idempotency key.
139/// They are the primary unit of work in the riglr job processing system.
140///
141/// ## Job Lifecycle
142///
143/// 1. **Creation** - Job is created with tool name and parameters
144/// 2. **Queuing** - Job is submitted to a job queue for processing
145/// 3. **Execution** - Worker picks up job and executes the corresponding tool
146/// 4. **Retry** - If execution fails with a retriable error, job may be retried
147/// 5. **Completion** - Job succeeds or fails permanently after exhausting retries
148///
149/// ## Examples
150///
151/// ### Basic Job Creation
152///
153/// ```rust
154/// use riglr_core::Job;
155/// use serde_json::json;
156///
157/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
158/// let job = Job::new(
159/// "price_checker",
160/// &json!({
161/// "symbol": "BTC",
162/// "currency": "USD"
163/// }),
164/// 3 // max retries
165/// )?;
166///
167/// println!("Job ID: {}", job.job_id);
168/// println!("Tool: {}", job.tool_name);
169/// println!("Can retry: {}", job.can_retry());
170/// # Ok(())
171/// # }
172/// ```
173///
174/// ### Idempotent Job Creation
175///
176/// ```rust
177/// use riglr_core::Job;
178/// use serde_json::json;
179///
180/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
181/// let job = Job::new_idempotent(
182/// "bank_transfer",
183/// &json!({
184/// "from_account": "123",
185/// "to_account": "456",
186/// "amount": 100.00,
187/// "currency": "USD"
188/// }),
189/// 2, // max retries
190/// "transfer_123_456_100_20241201" // idempotency key
191/// )?;
192///
193/// assert!(job.idempotency_key.is_some());
194/// # Ok(())
195/// # }
196/// ```
197#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct Job {
199 /// Unique identifier for this job instance.
200 ///
201 /// Generated automatically when the job is created. This ID is used
202 /// for tracking and logging purposes throughout the job's lifecycle.
203 pub job_id: Uuid,
204
205 /// Name of the tool to execute.
206 ///
207 /// This must match the name of a tool registered with the [`ToolWorker`](crate::ToolWorker).
208 /// Tool names also influence resource pool assignment (e.g., "solana_*" tools
209 /// use the "solana_rpc" resource pool).
210 pub tool_name: String,
211
212 /// Parameters to pass to the tool, serialized as JSON.
213 ///
214 /// These parameters will be passed to the tool's [`execute`](crate::Tool::execute)
215 /// method. Tools are responsible for validating and extracting the required
216 /// parameters from this JSON value.
217 pub params: serde_json::Value,
218
219 /// Optional idempotency key to prevent duplicate execution.
220 ///
221 /// When an idempotency key is provided and an idempotency store is configured,
222 /// the worker will cache successful results. Subsequent jobs with the same
223 /// idempotency key will return the cached result instead of re-executing.
224 ///
225 /// Idempotency keys should be unique per logical operation and include
226 /// relevant parameters to ensure uniqueness.
227 pub idempotency_key: Option<String>,
228
229 /// Maximum number of retry attempts allowed for this job.
230 ///
231 /// If a tool execution fails with a retriable error, the job will be
232 /// retried up to this many times. The total execution attempts will
233 /// be `max_retries + 1`.
234 ///
235 /// Only retriable failures trigger retries - permanent failures will
236 /// not be retried regardless of this setting.
237 pub max_retries: u32,
238
239 /// Current retry count for this job.
240 ///
241 /// This tracks how many retry attempts have been made. It starts at 0
242 /// for new jobs and is incremented after each failed execution attempt.
243 /// The job stops retrying when `retry_count >= max_retries`.
244 #[serde(default)]
245 pub retry_count: u32,
246}
247
248impl Job {
249 /// Create a new job without an idempotency key.
250 ///
251 /// This creates a standard job that will be executed normally without
252 /// result caching. If the job fails with a retriable error, it may be
253 /// retried up to `max_retries` times.
254 ///
255 /// # Parameters
256 /// * `tool_name` - Name of the tool to execute (must match a registered tool)
257 /// * `params` - Parameters to pass to the tool (will be JSON-serialized)
258 /// * `max_retries` - Maximum number of retry attempts (0 = no retries)
259 ///
260 /// # Returns
261 /// * `Ok(Job)` - Successfully created job
262 /// * `Err(serde_json::Error)` - If parameters cannot be serialized to JSON
263 ///
264 /// # Examples
265 ///
266 /// ```rust
267 /// use riglr_core::Job;
268 /// use serde_json::json;
269 ///
270 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
271 /// // Simple job with basic parameters
272 /// let job = Job::new(
273 /// "weather_check",
274 /// &json!({
275 /// "city": "San Francisco",
276 /// "units": "metric"
277 /// }),
278 /// 3 // allow up to 3 retries
279 /// )?;
280 ///
281 /// assert_eq!(job.tool_name, "weather_check");
282 /// assert_eq!(job.max_retries, 3);
283 /// assert_eq!(job.retry_count, 0);
284 /// assert!(job.idempotency_key.is_none());
285 /// # Ok(())
286 /// # }
287 /// ```
288 pub fn new<T: Serialize>(
289 tool_name: impl Into<String>,
290 params: &T,
291 max_retries: u32,
292 ) -> Result<Self, serde_json::Error> {
293 Ok(Self {
294 job_id: Uuid::new_v4(),
295 tool_name: tool_name.into(),
296 params: serde_json::to_value(params)?,
297 idempotency_key: None,
298 max_retries,
299 retry_count: 0,
300 })
301 }
302
303 /// Create a new job with an idempotency key for safe retry behavior.
304 ///
305 /// When an idempotency store is configured, successful results for jobs
306 /// with idempotency keys are cached. Subsequent jobs with the same key
307 /// will return the cached result instead of re-executing the tool.
308 ///
309 /// # Parameters
310 /// * `tool_name` - Name of the tool to execute (must match a registered tool)
311 /// * `params` - Parameters to pass to the tool (will be JSON-serialized)
312 /// * `max_retries` - Maximum number of retry attempts (0 = no retries)
313 /// * `idempotency_key` - Unique key for this operation (should include relevant parameters)
314 ///
315 /// # Returns
316 /// * `Ok(Job)` - Successfully created job
317 /// * `Err(serde_json::Error)` - If parameters cannot be serialized to JSON
318 ///
319 /// # Examples
320 ///
321 /// ```rust
322 /// use riglr_core::Job;
323 /// use serde_json::json;
324 ///
325 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
326 /// // Idempotent job for a financial transaction
327 /// let job = Job::new_idempotent(
328 /// "transfer_funds",
329 /// &json!({
330 /// "from": "account_123",
331 /// "to": "account_456",
332 /// "amount": 100.50,
333 /// "currency": "USD"
334 /// }),
335 /// 2, // allow up to 2 retries
336 /// "transfer_123_456_100.50_USD_20241201_001" // unique operation ID
337 /// )?;
338 ///
339 /// assert_eq!(job.tool_name, "transfer_funds");
340 /// assert_eq!(job.max_retries, 2);
341 /// assert!(job.idempotency_key.is_some());
342 /// # Ok(())
343 /// # }
344 /// ```
345 ///
346 /// # Idempotency Key Best Practices
347 ///
348 /// Idempotency keys should:
349 /// - Be unique per logical operation
350 /// - Include relevant parameters that affect the result
351 /// - Include timestamp or sequence numbers for time-sensitive operations
352 /// - Be deterministic for the same logical operation
353 ///
354 /// Example patterns:
355 /// - `"transfer_{from}_{to}_{amount}_{currency}_{date}_{sequence}"`
356 /// - `"price_check_{symbol}_{currency}_{timestamp}"`
357 /// - `"user_action_{user_id}_{action}_{target}_{date}"`
358 pub fn new_idempotent<T: Serialize>(
359 tool_name: impl Into<String>,
360 params: &T,
361 max_retries: u32,
362 idempotency_key: impl Into<String>,
363 ) -> Result<Self, serde_json::Error> {
364 Ok(Self {
365 job_id: Uuid::new_v4(),
366 tool_name: tool_name.into(),
367 params: serde_json::to_value(params)?,
368 idempotency_key: Some(idempotency_key.into()),
369 max_retries,
370 retry_count: 0,
371 })
372 }
373
374 /// Check if this job has retries remaining.
375 ///
376 /// Returns `true` if the job can be retried after a failure,
377 /// `false` if all retry attempts have been exhausted.
378 ///
379 /// # Returns
380 /// * `true` - Job can be retried (`retry_count < max_retries`)
381 /// * `false` - No retries remaining (`retry_count >= max_retries`)
382 ///
383 /// # Examples
384 ///
385 /// ```rust
386 /// use riglr_core::Job;
387 /// use serde_json::json;
388 ///
389 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
390 /// let mut job = Job::new("test_tool", &json!({}), 2)?;
391 ///
392 /// assert!(job.can_retry()); // 0 < 2, can retry
393 /// job.increment_retry();
394 /// assert!(job.can_retry()); // 1 < 2, can retry
395 /// job.increment_retry();
396 /// assert!(!job.can_retry()); // 2 >= 2, cannot retry
397 /// # Ok(())
398 /// # }
399 /// ```
400 pub fn can_retry(&self) -> bool {
401 self.retry_count < self.max_retries
402 }
403
404 /// Increment the retry count for this job.
405 ///
406 /// This should be called by the job processing system after each
407 /// failed execution attempt. The retry count is used to determine
408 /// whether the job can be retried again.
409 ///
410 /// # Examples
411 ///
412 /// ```rust
413 /// use riglr_core::Job;
414 /// use serde_json::json;
415 ///
416 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
417 /// let mut job = Job::new("test_tool", &json!({}), 3)?;
418 ///
419 /// assert_eq!(job.retry_count, 0);
420 /// job.increment_retry();
421 /// assert_eq!(job.retry_count, 1);
422 /// job.increment_retry();
423 /// assert_eq!(job.retry_count, 2);
424 /// # Ok(())
425 /// # }
426 /// ```
427 pub fn increment_retry(&mut self) {
428 self.retry_count += 1;
429 }
430}
431
432/// Result of executing a job, indicating success or failure with classification.
433///
434/// The `JobResult` enum provides structured representation of job execution outcomes,
435/// enabling the system to make intelligent decisions about error handling and retry logic.
436///
437/// ## Success vs Failure
438///
439/// - **Success**: The tool executed successfully and produced a result
440/// - **Failure**: The tool execution failed, with classification for retry behavior
441///
442/// ## Failure Classification
443///
444/// Failed executions are classified as either retriable or permanent:
445///
446/// - **Retriable failures**: Temporary issues that may resolve on retry
447/// - Network timeouts, connection errors
448/// - Rate limiting from external APIs
449/// - Temporary service unavailability
450/// - Resource contention
451///
452/// - **Permanent failures**: Issues that won't be resolved by retrying
453/// - Invalid parameters or malformed requests
454/// - Authentication/authorization failures
455/// - Insufficient funds or resources
456/// - Business logic violations
457///
458/// ## Examples
459///
460/// ### Creating Success Results
461///
462/// ```rust
463/// use riglr_core::JobResult;
464/// use serde_json::json;
465///
466/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
467/// // Simple success result
468/// let result = JobResult::success(&json!({
469/// "temperature": 72,
470/// "humidity": 65,
471/// "condition": "sunny"
472/// }))?;
473/// assert!(result.is_success());
474///
475/// // Success result with transaction hash
476/// let tx_result = JobResult::success_with_tx(
477/// &json!({
478/// "recipient": "0x123...",
479/// "amount": "1.5",
480/// "status": "confirmed"
481/// }),
482/// "0xabc123def456..."
483/// )?;
484/// assert!(tx_result.is_success());
485/// # Ok(())
486/// # }
487/// ```
488///
489/// ### Creating Failure Results
490///
491/// ```rust
492/// use riglr_core::{JobResult, ToolError};
493///
494/// // Retriable failure using new ToolError structure
495/// let network_error = JobResult::Failure {
496/// error: ToolError::retriable_string("Connection timeout after 30 seconds")
497/// };
498/// assert!(network_error.is_retriable());
499/// assert!(!network_error.is_success());
500///
501/// // Permanent failure using new ToolError structure
502/// let validation_error = JobResult::Failure {
503/// error: ToolError::permanent_string("Invalid email address format")
504/// };
505/// assert!(!validation_error.is_retriable());
506/// assert!(!validation_error.is_success());
507/// ```
508///
509/// ### Pattern Matching
510///
511/// ```rust
512/// use riglr_core::JobResult;
513///
514/// fn handle_result(result: JobResult) {
515/// match result {
516/// JobResult::Success { value, tx_hash } => {
517/// println!("Success: {:?}", value);
518/// if let Some(hash) = tx_hash {
519/// println!("Transaction: {}", hash);
520/// }
521/// }
522/// JobResult::Failure { error } => {
523/// if error.is_retriable() {
524/// println!("Temporary failure (will retry): {}", error);
525/// if let Some(delay) = error.retry_after() {
526/// println!("Retry after: {:?}", delay);
527/// }
528/// } else {
529/// println!("Permanent failure (won't retry): {}", error);
530/// }
531/// }
532/// }
533/// }
534/// ```
535#[derive(Debug, Serialize, Deserialize)]
536pub enum JobResult {
537 /// Job executed successfully and produced a result.
538 ///
539 /// This variant indicates that the tool completed its work successfully
540 /// and returned data. The result may optionally include a transaction
541 /// hash for blockchain operations.
542 Success {
543 /// The result data produced by the tool execution.
544 ///
545 /// This contains the actual output of the tool, serialized as JSON.
546 /// The structure of this value depends on the specific tool that
547 /// was executed.
548 value: serde_json::Value,
549
550 /// Optional transaction hash for blockchain operations.
551 ///
552 /// When the tool performs a blockchain transaction (transfer, swap, etc.),
553 /// this field should contain the transaction hash for tracking and
554 /// verification purposes. For non-blockchain operations, this is typically `None`.
555 tx_hash: Option<String>,
556 },
557
558 /// Job execution failed with error details and retry classification.
559 ///
560 /// This variant indicates that the tool execution failed. The ToolError
561 /// contains the error details and determines whether it's retriable.
562 Failure {
563 /// The structured error from tool execution.
564 ///
565 /// ToolError provides rich error classification including retriability,
566 /// rate limiting, and error context.
567 error: crate::error::ToolError,
568 },
569}
570
571/// Manual Clone implementation for JobResult
572///
573/// # Why Manual Implementation Is Necessary
574///
575/// A manual Clone implementation is required because `ToolError` contains a
576/// `source: Option<Arc<dyn Error + Send + Sync>>` field. The `dyn Error` trait object
577/// doesn't automatically implement Clone, which prevents the compiler from deriving
578/// Clone automatically for types containing it.
579///
580/// # Performance Characteristics
581///
582/// This implementation is extremely efficient:
583/// - For `Success` variants: Clones the JSON value and optional string (standard clones)
584/// - For `Failure` variants: Uses `Arc::clone` on the error's source field
585///
586/// The use of `Arc::clone` is critical - it's a cheap, pointer-copying operation that
587/// increments the reference count, NOT a deep clone of the error itself. This means
588/// cloning a JobResult with an error is as fast as cloning a pointer, regardless of
589/// the error's complexity.
590///
591/// # Maintenance Warning
592///
593/// If you modify the `ToolError` enum (adding new variants or fields), you MUST ensure
594/// the Clone implementation remains correct. There is an exhaustive "canary" test in
595/// `riglr-core/tests/arc_error_cloning_test.rs` that will fail to compile if the
596/// ToolError enum is modified without corresponding updates to its Clone logic.
597///
598/// Always run `cargo test --package riglr-core arc_error_cloning_test` after modifying
599/// ToolError to ensure your changes are properly handled.
600impl Clone for JobResult {
601 fn clone(&self) -> Self {
602 match self {
603 Self::Success { value, tx_hash } => Self::Success {
604 value: value.clone(),
605 tx_hash: tx_hash.clone(),
606 },
607 Self::Failure { error } => {
608 // Clone the ToolError - now cheap with Arc
609 let cloned_error = error.clone();
610 Self::Failure {
611 error: cloned_error,
612 }
613 }
614 }
615 }
616}
617
618impl JobResult {
619 /// Create a successful result without a transaction hash.
620 ///
621 /// This is the standard way to create success results for operations
622 /// that don't involve blockchain transactions.
623 ///
624 /// # Parameters
625 /// * `value` - The result data to serialize as JSON
626 ///
627 /// # Returns
628 /// * `Ok(JobResult::Success)` - Successfully created result
629 /// * `Err(serde_json::Error)` - If value cannot be serialized to JSON
630 ///
631 /// # Examples
632 ///
633 /// ```rust
634 /// use riglr_core::JobResult;
635 /// use serde_json::json;
636 ///
637 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
638 /// // API response result
639 /// let api_result = JobResult::success(&json!({
640 /// "data": {
641 /// "price": 45000.50,
642 /// "currency": "USD",
643 /// "timestamp": 1638360000
644 /// },
645 /// "status": "success"
646 /// }))?;
647 ///
648 /// assert!(api_result.is_success());
649 /// # Ok(())
650 /// # }
651 /// ```
652 pub fn success<T: Serialize>(value: &T) -> Result<Self, serde_json::Error> {
653 Ok(JobResult::Success {
654 value: serde_json::to_value(value)?,
655 tx_hash: None,
656 })
657 }
658
659 /// Create a successful result with a blockchain transaction hash.
660 ///
661 /// Use this for operations that involve blockchain transactions,
662 /// such as transfers, swaps, or contract interactions. The transaction
663 /// hash enables tracking and verification of the operation.
664 ///
665 /// # Parameters
666 /// * `value` - The result data to serialize as JSON
667 /// * `tx_hash` - The blockchain transaction hash
668 ///
669 /// # Returns
670 /// * `Ok(JobResult::Success)` - Successfully created result
671 /// * `Err(serde_json::Error)` - If value cannot be serialized to JSON
672 ///
673 /// # Examples
674 ///
675 /// ```rust
676 /// use riglr_core::JobResult;
677 /// use serde_json::json;
678 ///
679 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
680 /// // Solana transfer result
681 /// let transfer_result = JobResult::success_with_tx(
682 /// &json!({
683 /// "from": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM",
684 /// "to": "7XjBz3VSM8Y6kXoHdXFRvLV8Fn6dQJgj9AJhCFJZ9L2x",
685 /// "amount": 1.5,
686 /// "currency": "SOL",
687 /// "status": "confirmed"
688 /// }),
689 /// "2Z8h8K6wF5L9X3mE4u1nV7yQ9H5pG3rD1M2cB6x8Wq7N"
690 /// )?;
691 ///
692 /// // Ethereum swap result
693 /// let swap_result = JobResult::success_with_tx(
694 /// &json!({
695 /// "input_token": "USDC",
696 /// "output_token": "WETH",
697 /// "input_amount": "1000.00",
698 /// "output_amount": "0.42",
699 /// "slippage": "0.5%"
700 /// }),
701 /// "0x1234567890abcdef1234567890abcdef12345678"
702 /// )?;
703 ///
704 /// assert!(transfer_result.is_success());
705 /// assert!(swap_result.is_success());
706 /// # Ok(())
707 /// # }
708 /// ```
709 pub fn success_with_tx<T: Serialize>(
710 value: &T,
711 tx_hash: impl Into<String>,
712 ) -> Result<Self, serde_json::Error> {
713 Ok(JobResult::Success {
714 value: serde_json::to_value(value)?,
715 tx_hash: Some(tx_hash.into()),
716 })
717 }
718
719 /// Create a retriable failure result.
720 ///
721 /// Use this for temporary failures that may resolve on retry, such as
722 /// network timeouts, rate limits, or temporary service unavailability.
723 /// The worker will attempt to retry jobs that fail with retriable errors.
724 ///
725 /// # Parameters
726 /// * `error` - Human-readable error message describing the failure
727 ///
728 /// # Examples
729 ///
730 /// ```rust
731 /// use riglr_core::{JobResult, ToolError};
732 ///
733 /// // Network connectivity issues
734 /// let network_error = JobResult::Failure {
735 /// error: ToolError::retriable_string("Connection timeout after 30 seconds")
736 /// };
737 ///
738 /// // Rate limiting with retry delay
739 /// let rate_limit = JobResult::Failure {
740 /// error: ToolError::rate_limited_string("API rate limit exceeded")
741 /// };
742 ///
743 /// // Service unavailability
744 /// let service_down = JobResult::Failure {
745 /// error: ToolError::retriable_string("Service temporarily unavailable (HTTP 503)")
746 /// };
747 ///
748 /// assert!(network_error.is_retriable());
749 /// assert!(rate_limit.is_retriable());
750 /// assert!(service_down.is_retriable());
751 /// ```
752 /// Check if this result represents a successful execution.
753 ///
754 /// # Returns
755 /// * `true` - If this is a `JobResult::Success`
756 /// * `false` - If this is a `JobResult::Failure`
757 ///
758 /// # Examples
759 ///
760 /// ```rust
761 /// use riglr_core::{JobResult, ToolError};
762 /// use serde_json::json;
763 ///
764 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
765 /// let success = JobResult::success(&json!({"data": "ok"}))?;
766 /// let failure = JobResult::Failure {
767 /// error: ToolError::permanent_string("Error occurred")
768 /// };
769 ///
770 /// assert!(success.is_success());
771 /// assert!(!failure.is_success());
772 /// # Ok(())
773 /// # }
774 /// ```
775 pub fn is_success(&self) -> bool {
776 matches!(self, JobResult::Success { .. })
777 }
778
779 /// Check if this result represents a retriable failure.
780 ///
781 /// This method returns `true` only for `JobResult::Failure` variants
782 /// where `retriable` is `true`. Success results always return `false`.
783 ///
784 /// # Returns
785 /// * `true` - If this is a retriable failure
786 /// * `false` - If this is a success or permanent failure
787 ///
788 /// # Examples
789 ///
790 /// ```rust
791 /// use riglr_core::{JobResult, ToolError};
792 /// use serde_json::json;
793 ///
794 /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
795 /// let success = JobResult::success(&json!({"data": "ok"}))?;
796 /// let retriable = JobResult::Failure {
797 /// error: ToolError::retriable_string("Network timeout")
798 /// };
799 /// let permanent = JobResult::Failure {
800 /// error: ToolError::permanent_string("Invalid input")
801 /// };
802 ///
803 /// assert!(!success.is_retriable()); // Success is not retriable
804 /// assert!(retriable.is_retriable()); // Retriable failure
805 /// assert!(!permanent.is_retriable()); // Permanent failure is not retriable
806 /// # Ok(())
807 /// # }
808 /// ```
809 pub fn is_retriable(&self) -> bool {
810 match self {
811 JobResult::Failure { error } => error.is_retriable(),
812 _ => false,
813 }
814 }
815}
816
817#[cfg(test)]
818mod tests {
819 use super::*;
820
821 /// Compile-time test to ensure JobResult remains cloneable.
822 /// This test will fail to compile if JobResult or any of its fields
823 /// don't implement Clone, preventing the maintenance risk of manual
824 /// Clone implementations getting out of sync.
825 #[test]
826 fn test_job_result_clone_trait_bound() {
827 fn assert_clone<T: Clone>() {}
828
829 // Assert that JobResult implements Clone
830 assert_clone::<JobResult>();
831
832 // Assert that all field types implement Clone
833 assert_clone::<serde_json::Value>();
834 assert_clone::<Option<String>>();
835 assert_clone::<crate::error::ToolError>();
836
837 // Test actual cloning to ensure it works at runtime
838 let success = JobResult::Success {
839 value: serde_json::json!({"test": "data"}),
840 tx_hash: Some("0x123".to_string()),
841 };
842 let _cloned_success = success.clone();
843
844 let failure = JobResult::Failure {
845 error: crate::error::ToolError::retriable_string("test error"),
846 };
847 let _cloned_failure = failure.clone();
848 }
849
850 /// Test that cloning JobResult preserves error sources with Arc.
851 /// This ensures that error chains are preserved efficiently.
852 #[test]
853 fn test_job_result_clone_preserves_error_source() {
854 use std::error::Error;
855
856 // Create a source error
857 #[derive(Debug)]
858 struct SourceError(String);
859 impl std::fmt::Display for SourceError {
860 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
861 write!(f, "{}", self.0)
862 }
863 }
864 impl Error for SourceError {}
865
866 // Create a JobResult with an error containing a source
867 let error_with_source = crate::error::ToolError::retriable_with_source(
868 SourceError("original error".to_string()),
869 "operation failed",
870 );
871
872 let result = JobResult::Failure {
873 error: error_with_source,
874 };
875
876 // Clone the result
877 let cloned = result.clone();
878
879 // Verify the clone has the same error source
880 match (&result, &cloned) {
881 (JobResult::Failure { error: original }, JobResult::Failure { error: cloned_err }) => {
882 assert!(original.source().is_some());
883 assert!(cloned_err.source().is_some());
884 assert_eq!(
885 original.source().unwrap().to_string(),
886 cloned_err.source().unwrap().to_string()
887 );
888 }
889 _ => panic!("Expected both to be Failure variants"),
890 }
891 }
892
893 #[test]
894 fn test_job_creation() {
895 let params = serde_json::json!({"key": "value"});
896 let job = Job::new("test_tool", ¶ms, 3).unwrap();
897
898 assert_eq!(job.tool_name, "test_tool");
899 assert_eq!(job.params, params);
900 assert_eq!(job.max_retries, 3);
901 assert_eq!(job.retry_count, 0);
902 assert!(job.idempotency_key.is_none());
903 assert!(job.can_retry());
904 }
905
906 #[test]
907 fn test_job_new_when_valid_params_should_succeed() {
908 // Test with various parameter types
909 let string_params = "test string";
910 let job = Job::new("string_tool", &string_params, 5).unwrap();
911 assert_eq!(job.tool_name, "string_tool");
912 assert_eq!(job.max_retries, 5);
913 assert_eq!(job.retry_count, 0);
914 assert!(job.idempotency_key.is_none());
915 assert!(job.job_id != Uuid::nil());
916
917 // Test with complex JSON object
918 let complex_params = serde_json::json!({
919 "nested": {
920 "array": [1, 2, 3],
921 "string": "test",
922 "bool": true,
923 "null": null
924 }
925 });
926 let job = Job::new("complex_tool", &complex_params, 0).unwrap();
927 assert_eq!(job.params, complex_params);
928 assert_eq!(job.max_retries, 0);
929 }
930
931 #[test]
932 fn test_job_new_when_zero_retries_should_not_allow_retry() {
933 let job = Job::new("test_tool", &serde_json::json!({}), 0).unwrap();
934 assert_eq!(job.max_retries, 0);
935 assert!(!job.can_retry()); // 0 < 0 is false
936 }
937
938 #[test]
939 fn test_job_new_when_tool_name_is_string_slice_should_convert() {
940 let job = Job::new("slice_tool", &serde_json::json!({}), 1).unwrap();
941 assert_eq!(job.tool_name, "slice_tool");
942 }
943
944 #[test]
945 fn test_job_new_when_tool_name_is_string_should_convert() {
946 let tool_name = String::from("owned_tool");
947 let job = Job::new(tool_name, &serde_json::json!({}), 1).unwrap();
948 assert_eq!(job.tool_name, "owned_tool");
949 }
950
951 #[test]
952 fn test_job_with_idempotency() {
953 let params = serde_json::json!({"key": "value"});
954 let job = Job::new_idempotent("test_tool", ¶ms, 3, "test_key").unwrap();
955
956 assert_eq!(job.idempotency_key, Some("test_key".to_string()));
957 }
958
959 #[test]
960 fn test_job_new_idempotent_when_valid_params_should_succeed() {
961 let params = serde_json::json!({"transfer": "data"});
962 let job = Job::new_idempotent("idempotent_tool", ¶ms, 2, "unique_key_123").unwrap();
963
964 assert_eq!(job.tool_name, "idempotent_tool");
965 assert_eq!(job.params, params);
966 assert_eq!(job.max_retries, 2);
967 assert_eq!(job.retry_count, 0);
968 assert_eq!(job.idempotency_key, Some("unique_key_123".to_string()));
969 assert!(job.job_id != Uuid::nil());
970 assert!(job.can_retry());
971 }
972
973 #[test]
974 fn test_job_new_idempotent_when_key_is_string_slice_should_convert() {
975 let job = Job::new_idempotent("test_tool", &serde_json::json!({}), 1, "slice_key").unwrap();
976 assert_eq!(job.idempotency_key, Some("slice_key".to_string()));
977 }
978
979 #[test]
980 fn test_job_new_idempotent_when_key_is_string_should_convert() {
981 let key = String::from("owned_key");
982 let job = Job::new_idempotent("test_tool", &serde_json::json!({}), 1, key).unwrap();
983 assert_eq!(job.idempotency_key, Some("owned_key".to_string()));
984 }
985
986 #[test]
987 fn test_job_retry_logic() {
988 let params = serde_json::json!({"key": "value"});
989 let mut job = Job::new("test_tool", ¶ms, 2).unwrap();
990
991 assert!(job.can_retry());
992 job.increment_retry();
993 assert!(job.can_retry());
994 job.increment_retry();
995 assert!(!job.can_retry());
996 }
997
998 #[test]
999 fn test_job_can_retry_when_retry_count_equals_max_retries_should_return_false() {
1000 let mut job = Job::new("test_tool", &serde_json::json!({}), 1).unwrap();
1001 job.retry_count = 1; // Set equal to max_retries
1002 assert!(!job.can_retry());
1003 }
1004
1005 #[test]
1006 fn test_job_can_retry_when_retry_count_exceeds_max_retries_should_return_false() {
1007 let mut job = Job::new("test_tool", &serde_json::json!({}), 1).unwrap();
1008 job.retry_count = 2; // Set greater than max_retries
1009 assert!(!job.can_retry());
1010 }
1011
1012 #[test]
1013 fn test_job_increment_retry_when_called_multiple_times_should_increment() {
1014 let mut job = Job::new("test_tool", &serde_json::json!({}), 5).unwrap();
1015
1016 assert_eq!(job.retry_count, 0);
1017 job.increment_retry();
1018 assert_eq!(job.retry_count, 1);
1019 job.increment_retry();
1020 assert_eq!(job.retry_count, 2);
1021 job.increment_retry();
1022 assert_eq!(job.retry_count, 3);
1023 }
1024
1025 #[test]
1026 fn test_job_increment_retry_when_already_at_max_should_still_increment() {
1027 let mut job = Job::new("test_tool", &serde_json::json!({}), 1).unwrap();
1028 job.retry_count = 1;
1029
1030 job.increment_retry();
1031 assert_eq!(job.retry_count, 2); // Should still increment even if past max
1032 }
1033
1034 #[test]
1035 fn test_job_result_creation() {
1036 let success = JobResult::success(&"test_value").unwrap();
1037 assert!(success.is_success());
1038
1039 let success_with_tx = JobResult::success_with_tx(&"test_value", "tx_hash").unwrap();
1040 assert!(success_with_tx.is_success());
1041
1042 let retriable_failure = JobResult::Failure {
1043 error: crate::error::ToolError::retriable_string("test error"),
1044 };
1045 assert!(retriable_failure.is_retriable());
1046 assert!(!retriable_failure.is_success());
1047
1048 let permanent_failure = JobResult::Failure {
1049 error: crate::error::ToolError::permanent_string("test error"),
1050 };
1051 assert!(!permanent_failure.is_retriable());
1052 assert!(!permanent_failure.is_success());
1053 }
1054
1055 #[test]
1056 fn test_job_result_success_when_valid_value_should_create_success() {
1057 // Test with simple value
1058 let result = JobResult::success(&42).unwrap();
1059 match result {
1060 JobResult::Success {
1061 ref value,
1062 ref tx_hash,
1063 } => {
1064 assert_eq!(*value, serde_json::json!(42));
1065 assert!(tx_hash.is_none());
1066 }
1067 _ => panic!("Expected Success variant"),
1068 }
1069 assert!(result.is_success());
1070 assert!(!result.is_retriable());
1071
1072 // Test with complex object
1073 let complex_data = serde_json::json!({
1074 "status": "completed",
1075 "data": {
1076 "items": [1, 2, 3],
1077 "metadata": {
1078 "count": 3,
1079 "timestamp": "2024-01-01"
1080 }
1081 }
1082 });
1083 let result = JobResult::success(&complex_data).unwrap();
1084 match result {
1085 JobResult::Success { value, tx_hash } => {
1086 assert_eq!(value, complex_data);
1087 assert!(tx_hash.is_none());
1088 }
1089 _ => panic!("Expected Success variant"),
1090 }
1091 }
1092
1093 #[test]
1094 fn test_job_result_success_with_tx_when_valid_params_should_create_success() {
1095 let data = serde_json::json!({"amount": 100, "recipient": "0xabc"});
1096 let tx_hash = "0x123456789abcdef";
1097
1098 let result = JobResult::success_with_tx(&data, tx_hash).unwrap();
1099
1100 match result {
1101 JobResult::Success {
1102 ref value,
1103 tx_hash: ref hash,
1104 } => {
1105 assert_eq!(*value, data);
1106 assert_eq!(*hash, Some("0x123456789abcdef".to_string()));
1107 }
1108 _ => panic!("Expected Success variant"),
1109 }
1110 assert!(result.is_success());
1111 assert!(!result.is_retriable());
1112 }
1113
1114 #[test]
1115 fn test_job_result_success_with_tx_when_tx_hash_is_string_slice_should_convert() {
1116 let result = JobResult::success_with_tx(&"test", "slice_hash").unwrap();
1117 match result {
1118 JobResult::Success { tx_hash, .. } => {
1119 assert_eq!(tx_hash, Some("slice_hash".to_string()));
1120 }
1121 _ => panic!("Expected Success variant"),
1122 }
1123 }
1124
1125 #[test]
1126 fn test_job_result_success_with_tx_when_tx_hash_is_string_should_convert() {
1127 let hash = String::from("owned_hash");
1128 let result = JobResult::success_with_tx(&"test", hash).unwrap();
1129 match result {
1130 JobResult::Success { tx_hash, .. } => {
1131 assert_eq!(tx_hash, Some("owned_hash".to_string()));
1132 }
1133 _ => panic!("Expected Success variant"),
1134 }
1135 }
1136
1137 #[test]
1138 fn test_job_result_retriable_failure_when_error_message_should_create_retriable() {
1139 let error_msg = "Connection timeout occurred";
1140 let result = JobResult::Failure {
1141 error: crate::error::ToolError::retriable_string(error_msg),
1142 };
1143
1144 match result {
1145 JobResult::Failure { ref error } => {
1146 assert!(error.contains("Connection timeout occurred"));
1147 assert!(result.is_retriable());
1148 }
1149 _ => panic!("Expected Failure variant"),
1150 }
1151 assert!(!result.is_success());
1152 assert!(result.is_retriable());
1153 }
1154
1155 #[test]
1156 fn test_job_result_retriable_failure_when_error_is_string_slice_should_convert() {
1157 let result = JobResult::Failure {
1158 error: crate::error::ToolError::retriable_string("slice error"),
1159 };
1160 match result {
1161 JobResult::Failure { error } => {
1162 assert_eq!(
1163 error.to_string(),
1164 "Operation can be retried: slice error - slice error"
1165 );
1166 }
1167 _ => panic!("Expected Failure variant"),
1168 }
1169 }
1170
1171 #[test]
1172 fn test_job_result_retriable_failure_when_error_is_string_should_convert() {
1173 let error = String::from("owned error");
1174 let result = JobResult::Failure {
1175 error: crate::error::ToolError::retriable_string(error),
1176 };
1177 match result {
1178 JobResult::Failure { error } => {
1179 assert_eq!(
1180 error.to_string(),
1181 "Operation can be retried: owned error - owned error"
1182 );
1183 }
1184 _ => panic!("Expected Failure variant"),
1185 }
1186 }
1187
1188 #[test]
1189 fn test_job_result_permanent_failure_when_error_message_should_create_permanent() {
1190 let error_msg = "Invalid input parameters";
1191 let result = JobResult::Failure {
1192 error: crate::error::ToolError::permanent_string(error_msg),
1193 };
1194
1195 match result {
1196 JobResult::Failure { ref error } => {
1197 assert!(error.contains("Invalid input parameters"));
1198 assert!(!result.is_retriable());
1199 }
1200 _ => panic!("Expected Failure variant"),
1201 }
1202 assert!(!result.is_success());
1203 assert!(!result.is_retriable());
1204 }
1205
1206 #[test]
1207 fn test_job_result_permanent_failure_when_error_is_string_slice_should_convert() {
1208 let result = JobResult::Failure {
1209 error: crate::error::ToolError::permanent_string("slice error"),
1210 };
1211 match result {
1212 JobResult::Failure { error } => {
1213 assert_eq!(
1214 error.to_string(),
1215 "Permanent error: slice error - slice error"
1216 );
1217 }
1218 _ => panic!("Expected Failure variant"),
1219 }
1220 }
1221
1222 #[test]
1223 fn test_job_result_permanent_failure_when_error_is_string_should_convert() {
1224 let error = String::from("owned error");
1225 let result = JobResult::Failure {
1226 error: crate::error::ToolError::permanent_string(error),
1227 };
1228 match result {
1229 JobResult::Failure { error } => {
1230 assert_eq!(
1231 error.to_string(),
1232 "Permanent error: owned error - owned error"
1233 );
1234 }
1235 _ => panic!("Expected Failure variant"),
1236 }
1237 }
1238
1239 #[test]
1240 fn test_job_result_is_success_when_success_variant_should_return_true() {
1241 let success = JobResult::success(&"test").unwrap();
1242 assert!(success.is_success());
1243
1244 let success_with_tx = JobResult::success_with_tx(&"test", "hash").unwrap();
1245 assert!(success_with_tx.is_success());
1246 }
1247
1248 #[test]
1249 fn test_job_result_is_success_when_failure_variant_should_return_false() {
1250 let retriable_failure = JobResult::Failure {
1251 error: crate::error::ToolError::retriable_string("error"),
1252 };
1253 assert!(!retriable_failure.is_success());
1254
1255 let permanent_failure = JobResult::Failure {
1256 error: crate::error::ToolError::permanent_string("error"),
1257 };
1258 assert!(!permanent_failure.is_success());
1259 }
1260
1261 #[test]
1262 fn test_job_result_is_retriable_when_retriable_failure_should_return_true() {
1263 let result = JobResult::Failure {
1264 error: crate::error::ToolError::retriable_string("Network error"),
1265 };
1266 assert!(result.is_retriable());
1267 }
1268
1269 #[test]
1270 fn test_job_result_is_retriable_when_permanent_failure_should_return_false() {
1271 let result = JobResult::Failure {
1272 error: crate::error::ToolError::permanent_string("Invalid input"),
1273 };
1274 assert!(!result.is_retriable());
1275 }
1276
1277 #[test]
1278 fn test_job_result_is_retriable_when_success_should_return_false() {
1279 let result = JobResult::success(&"test").unwrap();
1280 assert!(!result.is_retriable());
1281
1282 let result_with_tx = JobResult::success_with_tx(&"test", "hash").unwrap();
1283 assert!(!result_with_tx.is_retriable());
1284 }
1285
1286 #[test]
1287 fn test_job_clone_should_create_identical_copy() {
1288 let original = Job::new_idempotent(
1289 "clone_tool",
1290 &serde_json::json!({"test": true}),
1291 3,
1292 "clone_key",
1293 )
1294 .unwrap();
1295 let cloned = original.clone();
1296
1297 assert_eq!(original.job_id, cloned.job_id);
1298 assert_eq!(original.tool_name, cloned.tool_name);
1299 assert_eq!(original.params, cloned.params);
1300 assert_eq!(original.idempotency_key, cloned.idempotency_key);
1301 assert_eq!(original.max_retries, cloned.max_retries);
1302 assert_eq!(original.retry_count, cloned.retry_count);
1303 }
1304
1305 #[test]
1306 fn test_job_result_clone_should_create_identical_copy() {
1307 let original_success =
1308 JobResult::success_with_tx(&serde_json::json!({"data": "test"}), "tx123").unwrap();
1309 let cloned_success = original_success.clone();
1310
1311 match (&original_success, &cloned_success) {
1312 (
1313 JobResult::Success {
1314 value: v1,
1315 tx_hash: h1,
1316 },
1317 JobResult::Success {
1318 value: v2,
1319 tx_hash: h2,
1320 },
1321 ) => {
1322 assert_eq!(v1, v2);
1323 assert_eq!(h1, h2);
1324 }
1325 _ => panic!("Expected Success variants"),
1326 }
1327
1328 let original_failure = JobResult::Failure {
1329 error: crate::error::ToolError::retriable_string("test error"),
1330 };
1331 let cloned_failure = original_failure.clone();
1332
1333 match (&original_failure, &cloned_failure) {
1334 (JobResult::Failure { error: e1 }, JobResult::Failure { error: e2 }) => {
1335 assert_eq!(e1, e2);
1336 }
1337 _ => panic!("Expected Failure variants"),
1338 }
1339 }
1340
1341 #[test]
1342 fn test_job_serialization_and_deserialization_should_preserve_data() {
1343 let original = Job::new_idempotent(
1344 "serialize_tool",
1345 &serde_json::json!({"key": "value"}),
1346 5,
1347 "serialize_key",
1348 )
1349 .unwrap();
1350
1351 // Serialize to JSON
1352 let serialized = serde_json::to_string(&original).unwrap();
1353
1354 // Deserialize back
1355 let deserialized: Job = serde_json::from_str(&serialized).unwrap();
1356
1357 assert_eq!(original.job_id, deserialized.job_id);
1358 assert_eq!(original.tool_name, deserialized.tool_name);
1359 assert_eq!(original.params, deserialized.params);
1360 assert_eq!(original.idempotency_key, deserialized.idempotency_key);
1361 assert_eq!(original.max_retries, deserialized.max_retries);
1362 assert_eq!(original.retry_count, deserialized.retry_count);
1363 }
1364
1365 #[test]
1366 fn test_job_result_serialization_and_deserialization_should_preserve_data() {
1367 // Test Success variant
1368 let original_success =
1369 JobResult::success_with_tx(&serde_json::json!({"amount": 100}), "hash123").unwrap();
1370 let serialized = serde_json::to_string(&original_success).unwrap();
1371 let deserialized: JobResult = serde_json::from_str(&serialized).unwrap();
1372
1373 match (&original_success, &deserialized) {
1374 (
1375 JobResult::Success {
1376 value: v1,
1377 tx_hash: h1,
1378 },
1379 JobResult::Success {
1380 value: v2,
1381 tx_hash: h2,
1382 },
1383 ) => {
1384 assert_eq!(v1, v2);
1385 assert_eq!(h1, h2);
1386 }
1387 _ => panic!("Expected Success variants"),
1388 }
1389
1390 // Test Failure variant
1391 let original_failure = JobResult::Failure {
1392 error: crate::error::ToolError::retriable_string("Network timeout"),
1393 };
1394 let serialized = serde_json::to_string(&original_failure).unwrap();
1395 let deserialized: JobResult = serde_json::from_str(&serialized).unwrap();
1396
1397 match (&original_failure, &deserialized) {
1398 (JobResult::Failure { error: e1 }, JobResult::Failure { error: e2 }) => {
1399 assert_eq!(e1, e2);
1400 }
1401 _ => panic!("Expected Failure variants"),
1402 }
1403 }
1404
1405 #[test]
1406 fn test_job_default_retry_count_when_deserializing_without_field_should_be_zero() {
1407 // Create JSON without retry_count field to test #[serde(default)]
1408 let json = r#"{
1409 "job_id": "550e8400-e29b-41d4-a716-446655440000",
1410 "tool_name": "test_tool",
1411 "params": {"key": "value"},
1412 "idempotency_key": null,
1413 "max_retries": 3
1414 }"#;
1415
1416 let job: Job = serde_json::from_str(json).unwrap();
1417 assert_eq!(job.retry_count, 0); // Should default to 0
1418 }
1419
1420 #[test]
1421 fn test_job_debug_format_should_include_all_fields() {
1422 let job = Job::new_idempotent(
1423 "debug_tool",
1424 &serde_json::json!({"test": "data"}),
1425 2,
1426 "debug_key",
1427 )
1428 .unwrap();
1429 let debug_output = format!("{:?}", job);
1430
1431 // Should contain all major field information
1432 assert!(debug_output.contains("job_id"));
1433 assert!(debug_output.contains("debug_tool"));
1434 assert!(debug_output.contains("test"));
1435 assert!(debug_output.contains("debug_key"));
1436 assert!(debug_output.contains("2")); // max_retries
1437 }
1438
1439 #[test]
1440 fn test_job_result_debug_format_should_include_variant_info() {
1441 let success =
1442 JobResult::success_with_tx(&serde_json::json!({"data": "test"}), "tx456").unwrap();
1443 let success_debug = format!("{:?}", success);
1444 assert!(success_debug.contains("Success"));
1445 assert!(success_debug.contains("tx456"));
1446
1447 let failure = JobResult::Failure {
1448 error: crate::error::ToolError::permanent_string("Debug error message"),
1449 };
1450 let failure_debug = format!("{:?}", failure);
1451 assert!(failure_debug.contains("Failure"));
1452 assert!(failure_debug.contains("Debug error message"));
1453 assert!(failure_debug.contains("Permanent")); // Permanent error variant
1454 }
1455}
1456
1457/// Transaction status tracking for job lifecycle
1458///
1459/// This enum represents the various states a transaction can be in during
1460/// its execution lifecycle, from initial submission through final confirmation.
1461#[derive(Debug, Clone)]
1462pub enum TransactionStatus {
1463 /// Transaction is pending submission
1464 Pending,
1465 /// Transaction has been submitted to the network
1466 Submitted {
1467 /// Transaction hash from the network
1468 hash: String,
1469 },
1470 /// Transaction is being confirmed
1471 Confirming {
1472 /// Transaction hash from the network
1473 hash: String,
1474 /// Current number of confirmations received
1475 confirmations: u64,
1476 },
1477 /// Transaction has been confirmed
1478 Confirmed {
1479 /// Transaction hash from the network
1480 hash: String,
1481 /// Block number where the transaction was included
1482 block: u64,
1483 },
1484 /// Transaction failed
1485 Failed {
1486 /// Reason why the transaction failed
1487 reason: String,
1488 },
1489}