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", &params, 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", &params, 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", &params, 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", &params, 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}