@@ -8,21 +8,29 @@ use taurus_core::fixtures::{Case, Cases, Input, RemoteFixture, print_failure, pr
88use taurus_core:: runtime:: engine:: ExecutionEngine ;
99use taurus_core:: runtime:: remote:: { RemoteExecution , RemoteRuntime } ;
1010use taurus_core:: types:: errors:: runtime_error:: RuntimeError ;
11- use tucana:: aquila:: action_node_value;
11+ use taurus_core:: types:: signal:: Signal ;
12+ use tucana:: aquila:: { ActionNodeSubFlowValue , action_node_value} ;
1213use tucana:: shared:: node_execution_result:: {
1314 Id as NodeExecutionResultId , Result as NodeExecutionOutcome ,
1415} ;
16+ use tucana:: shared:: value:: Kind ;
1517use tucana:: shared:: {
16- NodeExecutionResult ,
18+ NodeExecutionResult , Value ,
1719 helper:: value:: { from_json_value, to_json_value} ,
1820} ;
1921
20- struct FixtureRemoteRuntime {
22+ struct FixtureRemoteRuntime < ' a > {
2123 fixture : RemoteFixture ,
24+ // Needed to simulate an action invoking a minted `SubFlow` reference
25+ // back into `ExecutionEngine::execute_sub_flow` (see `sub_flow_calls`)
26+ // -- the same engine instance the flow itself is running on, so the
27+ // callback resolves against the registry entry the flow's own remote
28+ // call minted.
29+ engine : & ' a ExecutionEngine ,
2230}
2331
2432#[ async_trait:: async_trait]
25- impl RemoteRuntime for FixtureRemoteRuntime {
33+ impl RemoteRuntime for FixtureRemoteRuntime < ' _ > {
2634 async fn execute_remote (
2735 & self ,
2836 execution : RemoteExecution ,
@@ -48,6 +56,29 @@ impl RemoteRuntime for FixtureRemoteRuntime {
4856 ) ) ;
4957 }
5058
59+ // A `SubFlow`-valued parameter means this request carries a minted
60+ // callback reference (see `resolve_remote_args`/`SubFlowRegistry`)
61+ // -- drive it via `sub_flow_calls` regardless of how many entries
62+ // that list has (zero is a legitimate, real case: an empty input
63+ // list means the reference is minted but never actually invoked).
64+ // Only fall through to the literal-echo path below when no
65+ // `SubFlow` parameter is present at all.
66+ if let Some ( execution_identifier) = execution
67+ . request
68+ . parameters
69+ . iter ( )
70+ . find_map ( |parameter| match parameter. value . as_ref ( ) ? {
71+ action_node_value:: Value :: SubFlow ( ActionNodeSubFlowValue {
72+ execution_identifier,
73+ } ) => Some ( execution_identifier. clone ( ) ) ,
74+ _ => None ,
75+ } )
76+ {
77+ return self
78+ . drive_sub_flow_calls ( & execution, & execution_identifier)
79+ . await ;
80+ }
81+
5182 // Parameters are positional on the wire now (no key), so fixtures
5283 // with a `resultParameter` only make sense with a single remote
5384 // parameter — take it directly rather than looking it up by name.
@@ -65,7 +96,7 @@ impl RemoteRuntime for FixtureRemoteRuntime {
6596 "T-TEST-000003" ,
6697 "RemoteParameterMissing" ,
6798 format ! (
68- "Remote parameter {} was not provided" ,
99+ "Remote parameter {:? } was not provided" ,
69100 self . fixture. result_parameter
70101 ) ,
71102 )
@@ -83,6 +114,58 @@ impl RemoteRuntime for FixtureRemoteRuntime {
83114 }
84115}
85116
117+ impl FixtureRemoteRuntime < ' _ > {
118+ /// Simulates an action driving `ActionSubFlowExecutionRequest` traffic
119+ /// against `execution_identifier` (the request's minted `SubFlow`
120+ /// reference): one `execute_sub_flow` call per `sub_flow_calls` entry,
121+ /// in order -- exactly as `hercules`'s `for_each` implementation calls
122+ /// back once per list element via `executeSubFlow`. An empty
123+ /// `sub_flow_calls` is valid and drives zero calls (e.g. an empty input
124+ /// list: the reference is minted but never actually invoked). Fails the
125+ /// whole remote call the moment any individual sub-flow run fails,
126+ /// since a real action would abort the same way. Resolves to `null`,
127+ /// matching a `void`-signature consumer-driving remote function (e.g.
128+ /// `for_each` itself).
129+ async fn drive_sub_flow_calls (
130+ & self ,
131+ execution : & RemoteExecution ,
132+ execution_identifier : & str ,
133+ ) -> Result < NodeExecutionResult , RuntimeError > {
134+ for call in & self . fixture . sub_flow_calls {
135+ let value = from_json_value ( call. clone ( ) ) ;
136+ let report = self
137+ . engine
138+ . execute_sub_flow ( & execution_identifier, vec ! [ value] , Some ( self ) , false )
139+ . await
140+ . ok_or_else ( || {
141+ RuntimeError :: new (
142+ "T-TEST-000005" ,
143+ "SubFlowNotFound" ,
144+ format ! (
145+ "Sub flow {} was not minted (or already resolved)" ,
146+ execution_identifier
147+ ) ,
148+ )
149+ } ) ?;
150+ if let Signal :: Failure ( err) = report. signal {
151+ return Err ( err) ;
152+ }
153+ }
154+
155+ Ok ( NodeExecutionResult {
156+ started_at : 0 ,
157+ finished_at : 0 ,
158+ parameter_results : Vec :: new ( ) ,
159+ id : Some ( NodeExecutionResultId :: FunctionIdentifier (
160+ execution. request . function_identifier . clone ( ) ,
161+ ) ) ,
162+ result : Some ( NodeExecutionOutcome :: Success ( Value {
163+ kind : Some ( Kind :: NullValue ( 0 ) ) ,
164+ } ) ) ,
165+ } )
166+ }
167+ }
168+
86169pub enum CaseResult {
87170 Success ,
88171 Failure ( Input , serde_json:: Value ) ,
@@ -107,7 +190,10 @@ impl Testable for Case {
107190 let remote = self
108191 . remote
109192 . clone ( )
110- . map ( |fixture| FixtureRemoteRuntime { fixture } ) ;
193+ . map ( |fixture| FixtureRemoteRuntime {
194+ fixture,
195+ engine : & engine,
196+ } ) ;
111197
112198 for input in self . inputs . clone ( ) {
113199 let flow_input = input. clone ( ) . input . map ( from_json_value) ;
0 commit comments