Skip to main content

orcs_component/
child.rs

1//! Child trait for component-managed entities.
2//!
3//! Components may internally manage child entities. All children
4//! must implement this trait to ensure proper signal handling.
5//!
6//! # Why Child Trait?
7//!
8//! Without this constraint, Components could hold arbitrary entities
9//! that don't respond to signals. This would break the "Human as
10//! Superpower" principle - Veto signals must reach everything.
11//!
12//! # Child Hierarchy
13//!
14//! ```text
15//! ┌─────────────────────────────────────────────────────────────┐
16//! │                     Child (base trait)                       │
17//! │  - Identifiable + SignalReceiver + Statusable               │
18//! │  - Passive: managed by Component                             │
19//! └─────────────────────────────────────────────────────────────┘
20//!                              │
21//!                              ▼
22//! ┌─────────────────────────────────────────────────────────────┐
23//! │                   RunnableChild (extends Child)              │
24//! │  - Active: can execute work via run()                        │
25//! │  - Used for SubAgents, Workers, Skills                       │
26//! └─────────────────────────────────────────────────────────────┘
27//! ```
28//!
29//! # Component-Child Relationship
30//!
31//! ```text
32//! ┌─────────────────────────────────────────────┐
33//! │            Component (e.g., LlmComponent)    │
34//! │                                              │
35//! │  on_signal(Signal) {                        │
36//! │      // Forward to all children             │
37//! │      for child in &mut self.children {      │
38//! │          child.on_signal(&signal);          │
39//! │      }                                       │
40//! │  }                                           │
41//! │                                              │
42//! │  children: Vec<Box<dyn Child>>              │
43//! │  ┌────────┐  ┌────────┐  ┌────────┐        │
44//! │  │ Agent1 │  │ Agent2 │  │ Worker │        │
45//! │  │impl    │  │impl    │  │impl    │        │
46//! │  │ Child  │  │ Child  │  │ Child  │        │
47//! │  └────────┘  └────────┘  └────────┘        │
48//! └─────────────────────────────────────────────┘
49//! ```
50//!
51//! # Domain-Specific Implementations
52//!
53//! Concrete child types are defined in domain crates:
54//!
55//! - `orcs-llm`: `Agent impl Child`
56//! - `orcs-skill`: `Skill impl Child`
57//! - etc.
58//!
59//! # Example
60//!
61//! ```
62//! use orcs_component::{Child, Identifiable, SignalReceiver, Statusable, Status};
63//! use orcs_event::{Signal, SignalResponse};
64//!
65//! // Domain-specific child implementation
66//! struct MyAgent {
67//!     id: String,
68//!     status: Status,
69//! }
70//!
71//! impl Identifiable for MyAgent {
72//!     fn id(&self) -> &str {
73//!         &self.id
74//!     }
75//! }
76//!
77//! impl SignalReceiver for MyAgent {
78//!     fn on_signal(&mut self, signal: &Signal) -> SignalResponse {
79//!         if signal.is_veto() {
80//!             self.abort();
81//!             SignalResponse::Abort
82//!         } else {
83//!             SignalResponse::Ignored
84//!         }
85//!     }
86//!
87//!     fn abort(&mut self) {
88//!         self.status = Status::Aborted;
89//!     }
90//! }
91//!
92//! impl Statusable for MyAgent {
93//!     fn status(&self) -> Status {
94//!         self.status
95//!     }
96//! }
97//!
98//! // Mark as Child - now safe to hold in Component
99//! impl Child for MyAgent {}
100//! ```
101
102use crate::{Identifiable, SignalReceiver, Statusable};
103use async_trait::async_trait;
104use serde::{Deserialize, Serialize};
105use serde_json::Value;
106use thiserror::Error;
107
108// ============================================
109// Internal Error Type (thiserror-based)
110// ============================================
111
112/// Child execution errors.
113///
114/// Used internally for type-safe error handling.
115/// Convert to [`ChildResultDto`] for serialization.
116#[derive(Debug, Clone, Error)]
117pub enum ChildError {
118    /// Execution failed with a reason.
119    #[error("execution failed: {reason}")]
120    ExecutionFailed { reason: String },
121
122    /// Input validation failed.
123    #[error("invalid input: {0}")]
124    InvalidInput(String),
125
126    /// Timeout during execution.
127    #[error("timeout after {elapsed_ms}ms")]
128    Timeout { elapsed_ms: u64 },
129
130    /// Internal error.
131    #[error("internal: {0}")]
132    Internal(String),
133}
134
135impl ChildError {
136    /// Returns the error kind as a string identifier.
137    #[must_use]
138    pub fn kind(&self) -> &'static str {
139        match self {
140            Self::ExecutionFailed { .. } => "execution_failed",
141            Self::InvalidInput(_) => "invalid_input",
142            Self::Timeout { .. } => "timeout",
143            Self::Internal(_) => "internal",
144        }
145    }
146}
147
148/// A child entity managed by a Component.
149///
150/// All entities held inside a Component must implement this trait.
151/// This ensures they can:
152///
153/// - Be identified ([`Identifiable`])
154/// - Respond to signals ([`SignalReceiver`])
155/// - Report status ([`Statusable`])
156///
157/// # Object Safety
158///
159/// This trait is object-safe, allowing `Box<dyn Child>`.
160///
161/// # Signal Propagation
162///
163/// When a Component receives a signal, it MUST forward it to all children:
164///
165/// ```ignore
166/// fn on_signal(&mut self, signal: &Signal) -> SignalResponse {
167///     // Handle self first
168///     // ...
169///
170///     // Forward to ALL children
171///     for child in &mut self.children {
172///         child.on_signal(signal);
173///     }
174/// }
175/// ```
176///
177/// # Type Parameter Alternative
178///
179/// For stronger typing, Components can use:
180///
181/// ```ignore
182/// struct MyComponent<C: Child> {
183///     children: Vec<C>,
184/// }
185/// ```
186pub trait Child: Identifiable + SignalReceiver + Statusable + Send + Sync {
187    /// Inject a [`ChildContext`](crate::ChildContext) so the child can use `orcs.*` functions at
188    /// runtime (RPC, exec, spawn, file tools, etc.).
189    ///
190    /// The default implementation is a no-op — override when the child
191    /// actually needs context (e.g. `LuaChild`).
192    fn set_context(&mut self, _ctx: Box<dyn super::ChildContext>) {}
193}
194
195// ============================================
196// Internal Result Type
197// ============================================
198
199/// Result of a Child's work execution.
200///
201/// Represents the outcome of [`RunnableChild::run()`].
202/// For serialization, convert to [`ChildResultDto`] using `.into()`.
203///
204/// # Variants
205///
206/// - `Ok`: Work completed successfully with optional output data
207/// - `Err`: Work failed with a typed error
208/// - `Aborted`: Work was interrupted by a Signal (Veto/Cancel)
209///
210/// # Example
211///
212/// ```
213/// use orcs_component::{ChildResult, ChildError};
214/// use serde_json::json;
215///
216/// let success = ChildResult::Ok(json!({"processed": true}));
217/// assert!(success.is_ok());
218///
219/// let failure = ChildResult::Err(ChildError::Timeout { elapsed_ms: 5000 });
220/// assert!(failure.is_err());
221///
222/// let aborted = ChildResult::Aborted;
223/// assert!(aborted.is_aborted());
224/// ```
225#[derive(Debug, Clone)]
226pub enum ChildResult {
227    /// Work completed successfully.
228    Ok(Value),
229    /// Work failed with a typed error.
230    Err(ChildError),
231    /// Work was aborted by a Signal.
232    Aborted,
233}
234
235impl ChildResult {
236    /// Returns `true` if the result is `Ok`.
237    #[must_use]
238    pub fn is_ok(&self) -> bool {
239        matches!(self, Self::Ok(_))
240    }
241
242    /// Returns `true` if the result is `Err`.
243    #[must_use]
244    pub fn is_err(&self) -> bool {
245        matches!(self, Self::Err(_))
246    }
247
248    /// Returns `true` if the result is `Aborted`.
249    #[must_use]
250    pub fn is_aborted(&self) -> bool {
251        matches!(self, Self::Aborted)
252    }
253
254    /// Returns the success value if `Ok`, otherwise `None`.
255    #[must_use]
256    pub fn ok(self) -> Option<Value> {
257        match self {
258            Self::Ok(v) => Some(v),
259            _ => None,
260        }
261    }
262
263    /// Returns the error if `Err`, otherwise `None`.
264    #[must_use]
265    pub fn err(self) -> Option<ChildError> {
266        match self {
267            Self::Err(e) => Some(e),
268            _ => None,
269        }
270    }
271}
272
273impl Default for ChildResult {
274    fn default() -> Self {
275        Self::Ok(Value::Null)
276    }
277}
278
279impl From<Value> for ChildResult {
280    fn from(value: Value) -> Self {
281        Self::Ok(value)
282    }
283}
284
285impl From<ChildError> for ChildResult {
286    fn from(err: ChildError) -> Self {
287        Self::Err(err)
288    }
289}
290
291// ============================================
292// External DTO Type (Serializable)
293// ============================================
294
295/// Serializable representation of [`ChildResult`].
296///
297/// Use this type for IPC, persistence, or JSON serialization.
298///
299/// # Example
300///
301/// ```
302/// use orcs_component::{ChildResult, ChildResultDto, ChildError};
303/// use serde_json::json;
304///
305/// let result = ChildResult::Err(ChildError::Timeout { elapsed_ms: 5000 });
306/// let dto: ChildResultDto = result.into();
307///
308/// let json = serde_json::to_string(&dto).expect("ChildResultDto should serialize");
309/// assert!(json.contains("timeout"));
310/// ```
311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
312pub enum ChildResultDto {
313    /// Work completed successfully.
314    Ok(Value),
315    /// Work failed with error details.
316    Err {
317        /// Error kind identifier (e.g., "timeout", "invalid_input").
318        kind: String,
319        /// Human-readable error message.
320        message: String,
321    },
322    /// Work was aborted by a Signal.
323    Aborted,
324}
325
326impl ChildResultDto {
327    /// Returns `true` if the result is `Ok`.
328    #[must_use]
329    pub fn is_ok(&self) -> bool {
330        matches!(self, Self::Ok(_))
331    }
332
333    /// Returns `true` if the result is `Err`.
334    #[must_use]
335    pub fn is_err(&self) -> bool {
336        matches!(self, Self::Err { .. })
337    }
338
339    /// Returns `true` if the result is `Aborted`.
340    #[must_use]
341    pub fn is_aborted(&self) -> bool {
342        matches!(self, Self::Aborted)
343    }
344}
345
346impl From<ChildResult> for ChildResultDto {
347    fn from(result: ChildResult) -> Self {
348        match result {
349            ChildResult::Ok(v) => Self::Ok(v),
350            ChildResult::Err(e) => Self::Err {
351                kind: e.kind().to_string(),
352                message: e.to_string(),
353            },
354            ChildResult::Aborted => Self::Aborted,
355        }
356    }
357}
358
359impl Default for ChildResultDto {
360    fn default() -> Self {
361        Self::Ok(Value::Null)
362    }
363}
364
365/// A Child that can actively execute work.
366///
367/// Extends [`Child`] with the ability to run tasks.
368/// Used for SubAgents, Workers, and Skills that need to
369/// perform actual work rather than just respond to signals.
370///
371/// # Architecture
372///
373/// ```text
374/// Component (Manager)
375///     │
376///     │ spawn_child()
377///     ▼
378/// RunnableChild (Worker)
379///     │
380///     │ run(input)
381///     ▼
382/// ChildResult
383/// ```
384///
385/// # Signal Handling
386///
387/// During `run()`, the child should periodically check for
388/// signals and return `ChildResult::Aborted` if a Veto/Cancel
389/// is received.
390///
391/// # Example
392///
393/// ```
394/// use orcs_component::{Child, RunnableChild, ChildResult, Identifiable, SignalReceiver, Statusable, Status};
395/// use orcs_event::{Signal, SignalResponse};
396/// use serde_json::{json, Value};
397///
398/// struct Worker {
399///     id: String,
400///     status: Status,
401/// }
402///
403/// impl Identifiable for Worker {
404///     fn id(&self) -> &str { &self.id }
405/// }
406///
407/// impl SignalReceiver for Worker {
408///     fn on_signal(&mut self, signal: &Signal) -> SignalResponse {
409///         if signal.is_veto() {
410///             self.status = Status::Aborted;
411///             SignalResponse::Abort
412///         } else {
413///             SignalResponse::Handled
414///         }
415///     }
416///     fn abort(&mut self) { self.status = Status::Aborted; }
417/// }
418///
419/// impl Statusable for Worker {
420///     fn status(&self) -> Status { self.status }
421/// }
422///
423/// impl Child for Worker {}
424///
425/// impl RunnableChild for Worker {
426///     fn run(&mut self, input: Value) -> ChildResult {
427///         self.status = Status::Running;
428///         // Do work...
429///         let result = json!({"input": input, "processed": true});
430///         self.status = Status::Idle;
431///         ChildResult::Ok(result)
432///     }
433/// }
434/// ```
435pub trait RunnableChild: Child {
436    /// Execute work with the given input (synchronous).
437    ///
438    /// # Arguments
439    ///
440    /// * `input` - Input data for the work (JSON value)
441    ///
442    /// # Returns
443    ///
444    /// - `ChildResult::Ok(value)` on success
445    /// - `ChildResult::Err(error)` on failure
446    /// - `ChildResult::Aborted` if interrupted by signal
447    ///
448    /// # Contract
449    ///
450    /// - Should update status to `Running` at start
451    /// - Should update status to `Idle` or `Completed` on success
452    /// - Should periodically check for signals during long operations
453    fn run(&mut self, input: Value) -> ChildResult;
454}
455
456/// Async version of [`RunnableChild`].
457///
458/// Use this trait for children that perform async I/O operations
459/// such as LLM API calls, network requests, or file I/O.
460///
461/// # Example
462///
463/// ```ignore
464/// use orcs_component::{AsyncRunnableChild, ChildResult, Child};
465/// use async_trait::async_trait;
466/// use serde_json::Value;
467///
468/// struct LlmWorker {
469///     // ...
470/// }
471///
472/// #[async_trait]
473/// impl AsyncRunnableChild for LlmWorker {
474///     async fn run(&mut self, input: Value) -> ChildResult {
475///         // Async LLM call
476///         let response = self.client.complete(&input).await?;
477///         ChildResult::Ok(response)
478///     }
479/// }
480/// ```
481///
482/// # When to Use
483///
484/// | Trait | Use Case |
485/// |-------|----------|
486/// | `RunnableChild` | CPU-bound, quick tasks |
487/// | `AsyncRunnableChild` | I/O-bound, network, LLM calls |
488#[async_trait]
489pub trait AsyncRunnableChild: Child {
490    /// Execute work with the given input (asynchronous).
491    ///
492    /// # Arguments
493    ///
494    /// * `input` - Input data for the work (JSON value)
495    ///
496    /// # Returns
497    ///
498    /// - `ChildResult::Ok(value)` on success
499    /// - `ChildResult::Err(error)` on failure
500    /// - `ChildResult::Aborted` if interrupted by signal
501    ///
502    /// # Contract
503    ///
504    /// - Should update status to `Running` at start
505    /// - Should update status to `Idle` or `Completed` on success
506    /// - Should check for cancellation using `tokio::select!` or similar
507    async fn run(&mut self, input: Value) -> ChildResult;
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513    use crate::Status;
514    use orcs_event::{Signal, SignalResponse};
515
516    struct TestChild {
517        id: String,
518        status: Status,
519    }
520
521    impl Identifiable for TestChild {
522        fn id(&self) -> &str {
523            &self.id
524        }
525    }
526
527    impl SignalReceiver for TestChild {
528        fn on_signal(&mut self, _signal: &Signal) -> SignalResponse {
529            SignalResponse::Ignored
530        }
531
532        fn abort(&mut self) {
533            self.status = Status::Aborted;
534        }
535    }
536
537    impl Statusable for TestChild {
538        fn status(&self) -> Status {
539            self.status
540        }
541    }
542
543    impl Child for TestChild {}
544
545    #[test]
546    fn child_object_safety() {
547        let child: Box<dyn Child> = Box::new(TestChild {
548            id: "test".into(),
549            status: Status::Idle,
550        });
551
552        assert_eq!(child.id(), "test");
553        assert_eq!(child.status(), Status::Idle);
554    }
555
556    #[test]
557    fn child_collection() {
558        let children: Vec<Box<dyn Child>> = vec![
559            Box::new(TestChild {
560                id: "child-1".into(),
561                status: Status::Running,
562            }),
563            Box::new(TestChild {
564                id: "child-2".into(),
565                status: Status::Idle,
566            }),
567        ];
568
569        assert_eq!(children.len(), 2);
570        assert_eq!(children[0].id(), "child-1");
571        assert_eq!(children[1].id(), "child-2");
572    }
573
574    // --- ChildError tests ---
575
576    #[test]
577    fn child_error_kind() {
578        assert_eq!(
579            ChildError::ExecutionFailed { reason: "x".into() }.kind(),
580            "execution_failed"
581        );
582        assert_eq!(ChildError::InvalidInput("x".into()).kind(), "invalid_input");
583        assert_eq!(ChildError::Timeout { elapsed_ms: 100 }.kind(), "timeout");
584        assert_eq!(ChildError::Internal("x".into()).kind(), "internal");
585    }
586
587    #[test]
588    fn child_error_display() {
589        let err = ChildError::Timeout { elapsed_ms: 5000 };
590        assert_eq!(err.to_string(), "timeout after 5000ms");
591    }
592
593    // --- ChildResult tests ---
594
595    #[test]
596    fn child_result_ok() {
597        let result = ChildResult::Ok(serde_json::json!({"done": true}));
598        assert!(result.is_ok());
599        assert!(!result.is_err());
600        assert!(!result.is_aborted());
601    }
602
603    #[test]
604    fn child_result_err() {
605        let result = ChildResult::Err(ChildError::ExecutionFailed {
606            reason: "failed".into(),
607        });
608        assert!(!result.is_ok());
609        assert!(result.is_err());
610        assert!(!result.is_aborted());
611    }
612
613    #[test]
614    fn child_result_aborted() {
615        let result = ChildResult::Aborted;
616        assert!(!result.is_ok());
617        assert!(!result.is_err());
618        assert!(result.is_aborted());
619    }
620
621    #[test]
622    fn child_result_ok_extract() {
623        let result = ChildResult::Ok(serde_json::json!(42));
624        let value = result.ok();
625        assert_eq!(value, Some(serde_json::json!(42)));
626    }
627
628    #[test]
629    fn child_result_err_extract() {
630        let result = ChildResult::Err(ChildError::InvalidInput("bad input".into()));
631        let err = result.err();
632        assert!(err.is_some());
633        assert_eq!(
634            err.expect("ChildResult::Err should yield Some(ChildError)")
635                .kind(),
636            "invalid_input"
637        );
638    }
639
640    #[test]
641    fn child_result_default() {
642        let result = ChildResult::default();
643        assert!(result.is_ok());
644        assert_eq!(result.ok(), Some(Value::Null));
645    }
646
647    #[test]
648    fn child_result_from_value() {
649        let result: ChildResult = serde_json::json!({"key": "value"}).into();
650        assert!(result.is_ok());
651    }
652
653    #[test]
654    fn child_result_from_error() {
655        let result: ChildResult = ChildError::Internal("oops".into()).into();
656        assert!(result.is_err());
657    }
658
659    // --- ChildResultDto tests ---
660
661    #[test]
662    fn child_result_dto_from_ok() {
663        let result = ChildResult::Ok(serde_json::json!({"done": true}));
664        let dto: ChildResultDto = result.into();
665
666        assert!(dto.is_ok());
667        assert!(!dto.is_err());
668        assert!(!dto.is_aborted());
669    }
670
671    #[test]
672    fn child_result_dto_from_err() {
673        let result = ChildResult::Err(ChildError::Timeout { elapsed_ms: 3000 });
674        let dto: ChildResultDto = result.into();
675
676        assert!(dto.is_err());
677        if let ChildResultDto::Err { kind, message } = dto {
678            assert_eq!(kind, "timeout");
679            assert!(message.contains("3000"));
680        } else {
681            panic!("expected Err variant");
682        }
683    }
684
685    #[test]
686    fn child_result_dto_from_aborted() {
687        let result = ChildResult::Aborted;
688        let dto: ChildResultDto = result.into();
689
690        assert!(dto.is_aborted());
691    }
692
693    #[test]
694    fn child_result_dto_serialization_err() {
695        let dto = ChildResultDto::Err {
696            kind: "timeout".into(),
697            message: "timeout after 5000ms".into(),
698        };
699
700        let json =
701            serde_json::to_string(&dto).expect("ChildResultDto::Err should serialize to JSON");
702        let restored: ChildResultDto =
703            serde_json::from_str(&json).expect("ChildResultDto::Err should deserialize from JSON");
704
705        assert_eq!(dto, restored);
706    }
707
708    #[test]
709    fn child_result_dto_serialization_ok_roundtrip() {
710        let original = ChildResultDto::Ok(serde_json::json!({"key": "value", "count": 42}));
711        let json =
712            serde_json::to_string(&original).expect("ChildResultDto::Ok should serialize to JSON");
713        let restored: ChildResultDto =
714            serde_json::from_str(&json).expect("ChildResultDto::Ok should deserialize from JSON");
715
716        assert_eq!(original, restored);
717    }
718
719    #[test]
720    fn child_result_dto_serialization_aborted_roundtrip() {
721        let original = ChildResultDto::Aborted;
722        let json = serde_json::to_string(&original)
723            .expect("ChildResultDto::Aborted should serialize to JSON");
724        let restored: ChildResultDto = serde_json::from_str(&json)
725            .expect("ChildResultDto::Aborted should deserialize from JSON");
726
727        assert_eq!(original, restored);
728    }
729
730    #[test]
731    fn child_result_to_dto_roundtrip() {
732        // ChildResult → ChildResultDto → JSON → ChildResultDto
733        let result = ChildResult::Err(ChildError::ExecutionFailed {
734            reason: "network error".into(),
735        });
736        let dto: ChildResultDto = result.into();
737        let json = serde_json::to_string(&dto)
738            .expect("ChildResultDto from ChildResult::Err should serialize to JSON");
739        let restored: ChildResultDto = serde_json::from_str(&json)
740            .expect("ChildResultDto should deserialize from JSON in roundtrip test");
741
742        assert_eq!(dto, restored);
743        if let ChildResultDto::Err { kind, message } = restored {
744            assert_eq!(kind, "execution_failed");
745            assert!(message.contains("network error"));
746        }
747    }
748
749    #[test]
750    fn child_result_dto_default() {
751        let dto = ChildResultDto::default();
752        assert!(dto.is_ok());
753    }
754
755    // --- RunnableChild tests ---
756
757    struct TestRunnableChild {
758        id: String,
759        status: Status,
760    }
761
762    impl Identifiable for TestRunnableChild {
763        fn id(&self) -> &str {
764            &self.id
765        }
766    }
767
768    impl SignalReceiver for TestRunnableChild {
769        fn on_signal(&mut self, signal: &Signal) -> SignalResponse {
770            if signal.is_veto() {
771                self.status = Status::Aborted;
772                SignalResponse::Abort
773            } else {
774                SignalResponse::Handled
775            }
776        }
777
778        fn abort(&mut self) {
779            self.status = Status::Aborted;
780        }
781    }
782
783    impl Statusable for TestRunnableChild {
784        fn status(&self) -> Status {
785            self.status
786        }
787    }
788
789    impl Child for TestRunnableChild {}
790
791    impl RunnableChild for TestRunnableChild {
792        fn run(&mut self, input: Value) -> ChildResult {
793            self.status = Status::Running;
794            // Simulate work
795            let result = serde_json::json!({
796                "input": input,
797                "processed": true
798            });
799            self.status = Status::Idle;
800            ChildResult::Ok(result)
801        }
802    }
803
804    #[test]
805    fn runnable_child_run() {
806        let mut child = TestRunnableChild {
807            id: "worker-1".into(),
808            status: Status::Idle,
809        };
810
811        let input = serde_json::json!({"task": "test"});
812        let result = child.run(input.clone());
813
814        assert!(result.is_ok());
815        if let ChildResult::Ok(value) = result {
816            assert_eq!(value["input"], input);
817            assert_eq!(value["processed"], true);
818        }
819        assert_eq!(child.status(), Status::Idle);
820    }
821
822    #[test]
823    fn runnable_child_object_safety() {
824        let child: Box<dyn RunnableChild> = Box::new(TestRunnableChild {
825            id: "worker".into(),
826            status: Status::Idle,
827        });
828
829        // Can be used as dyn RunnableChild
830        assert_eq!(child.id(), "worker");
831        assert_eq!(child.status(), Status::Idle);
832    }
833
834    // --- AsyncRunnableChild tests ---
835
836    struct TestAsyncChild {
837        id: String,
838        status: Status,
839    }
840
841    impl Identifiable for TestAsyncChild {
842        fn id(&self) -> &str {
843            &self.id
844        }
845    }
846
847    impl SignalReceiver for TestAsyncChild {
848        fn on_signal(&mut self, signal: &Signal) -> SignalResponse {
849            if signal.is_veto() {
850                self.status = Status::Aborted;
851                SignalResponse::Abort
852            } else {
853                SignalResponse::Handled
854            }
855        }
856
857        fn abort(&mut self) {
858            self.status = Status::Aborted;
859        }
860    }
861
862    impl Statusable for TestAsyncChild {
863        fn status(&self) -> Status {
864            self.status
865        }
866    }
867
868    impl Child for TestAsyncChild {}
869
870    #[async_trait]
871    impl AsyncRunnableChild for TestAsyncChild {
872        async fn run(&mut self, input: Value) -> ChildResult {
873            self.status = Status::Running;
874            // Simulate async work
875            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
876            let result = serde_json::json!({
877                "input": input,
878                "async": true
879            });
880            self.status = Status::Idle;
881            ChildResult::Ok(result)
882        }
883    }
884
885    #[tokio::test]
886    async fn async_runnable_child_run() {
887        let mut child = TestAsyncChild {
888            id: "async-worker-1".into(),
889            status: Status::Idle,
890        };
891
892        let input = serde_json::json!({"task": "async_test"});
893        let result = child.run(input.clone()).await;
894
895        assert!(result.is_ok());
896        if let ChildResult::Ok(value) = result {
897            assert_eq!(value["input"], input);
898            assert_eq!(value["async"], true);
899        }
900        assert_eq!(child.status(), Status::Idle);
901    }
902
903    #[tokio::test]
904    async fn async_runnable_child_object_safety() {
905        let child: Box<dyn AsyncRunnableChild> = Box::new(TestAsyncChild {
906            id: "async-worker".into(),
907            status: Status::Idle,
908        });
909
910        // Can be used as dyn AsyncRunnableChild
911        assert_eq!(child.id(), "async-worker");
912        assert_eq!(child.status(), Status::Idle);
913    }
914}