Skip to content

Commit dff684d

Browse files
committed
feat: added more fixtures
1 parent 63fe136 commit dff684d

7 files changed

Lines changed: 456 additions & 10 deletions

File tree

crates/taurus-core/src/fixtures.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,20 @@ pub struct Case {
3131
pub struct RemoteFixture {
3232
pub target_service: String,
3333
pub function_identifier: String,
34-
pub result_parameter: String,
34+
/// Only meaningful when `sub_flow_calls` is empty: the literal-valued
35+
/// parameter to echo straight back as this remote call's own result
36+
/// (see `0012_remote_function_subflow.json`).
37+
#[serde(default)]
38+
pub result_parameter: Option<String>,
39+
/// Positional values to drive one `ExecutionEngine::execute_sub_flow`
40+
/// call per entry against this request's `SubFlow`-valued parameter, in
41+
/// order -- simulates an action invoking a minted sub-flow reference
42+
/// the same number of times a real action would (e.g. once per element
43+
/// for a remotely-dispatched `for_each`'s consumer callback). When
44+
/// non-empty, the remote call itself resolves to `null` once every call
45+
/// has run, mirroring a `void`-signature remote function.
46+
#[serde(default)]
47+
pub sub_flow_calls: Vec<serde_json::Value>,
3548
}
3649

3750
#[derive(Clone, Deserialize)]

crates/taurus-core/src/runtime/engine/sub_flow_registry.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,13 +153,18 @@ mod tests {
153153
let flow = flow_with_node(7);
154154
let activity = Arc::new(Notify::new());
155155

156+
// caller_node_id and caller_parameter_index are deliberately distinct
157+
// (3, 5) so a field mix-up in `mint`/`PendingSubFlow` would be
158+
// caught by the assertions below instead of accidentally matching.
156159
let id = registry
157-
.mint(&flow, 7, "parent-1", activity, 1, 1)
160+
.mint(&flow, 7, "parent-1", activity, 3, 5)
158161
.expect("node 7 exists in flow");
159162

160163
let pending = registry.get(&id).expect("entry should exist after mint");
161164
assert_eq!(pending.start_idx, 0);
162165
assert_eq!(pending.parent_execution_id, "parent-1");
166+
assert_eq!(pending.caller_node_id, 3);
167+
assert_eq!(pending.caller_parameter_index, 5);
163168

164169
// Looking up again does not remove the entry.
165170
assert!(registry.get(&id).is_some());

crates/taurus-tests/src/main.rs

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,29 @@ use taurus_core::fixtures::{Case, Cases, Input, RemoteFixture, print_failure, pr
88
use taurus_core::runtime::engine::ExecutionEngine;
99
use taurus_core::runtime::remote::{RemoteExecution, RemoteRuntime};
1010
use 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};
1213
use tucana::shared::node_execution_result::{
1314
Id as NodeExecutionResultId, Result as NodeExecutionOutcome,
1415
};
16+
use tucana::shared::value::Kind;
1517
use 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+
86169
pub 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);

crates/taurus/src/app/worker.rs

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -827,11 +827,12 @@ mod tests {
827827
/// keyed here against the node/parameter that minted the sub-flow
828828
/// reference in the first place (node `caller_node_id`'s parameter at
829829
/// `caller_parameter_index`).
830-
fn first_input_type_param(
830+
fn input_type_param(
831831
database_id: i64,
832832
runtime_parameter_id: &str,
833833
caller_node_id: i64,
834834
caller_parameter_index: i64,
835+
input_index: i64,
835836
) -> NodeParameter {
836837
NodeParameter {
837838
database_id,
@@ -843,7 +844,7 @@ mod tests {
843844
tucana::shared::InputType {
844845
node_id: caller_node_id,
845846
parameter_index: caller_parameter_index,
846-
input_index: 0,
847+
input_index,
847848
},
848849
)),
849850
paths: vec![],
@@ -854,6 +855,21 @@ mod tests {
854855
}
855856
}
856857

858+
fn first_input_type_param(
859+
database_id: i64,
860+
runtime_parameter_id: &str,
861+
caller_node_id: i64,
862+
caller_parameter_index: i64,
863+
) -> NodeParameter {
864+
input_type_param(
865+
database_id,
866+
runtime_parameter_id,
867+
caller_node_id,
868+
caller_parameter_index,
869+
0,
870+
)
871+
}
872+
857873
/// Captures the minted sub-flow UUID from the outgoing remote request's
858874
/// `SubFlow` parameter, then blocks until `release` is notified --
859875
/// standing in for the parent remote call staying outstanding while the
@@ -1006,6 +1022,83 @@ mod tests {
10061022
}
10071023
}
10081024

1025+
/// A sub-flow's action-supplied parameters are seeded one `InputType`
1026+
/// slot per positional value (`input_index` 0, 1, ...), not just the
1027+
/// first. Drives a two-argument call and reads both slots back through
1028+
/// `std::number::add`, so a regression that only seeds `input_index: 0`
1029+
/// (as a naive port of the single-argument `for_each` case might) would
1030+
/// fail this by treating the second argument as missing.
1031+
#[tokio::test]
1032+
async fn sub_flow_execution_seeds_every_positional_input_index_independently() {
1033+
let engine = Arc::new(ExecutionEngine::new());
1034+
let minted_id = Arc::new(StdMutex::new(None));
1035+
let release = Arc::new(Notify::new());
1036+
let remote = BlockingMintCapturingRuntime {
1037+
result: NodeExecutionResult {
1038+
started_at: 1,
1039+
finished_at: 2,
1040+
parameter_results: Vec::new(),
1041+
id: Some(tucana::shared::node_execution_result::Id::NodeId(1)),
1042+
result: Some(tucana::shared::node_execution_result::Result::Success(
1043+
int_value(1),
1044+
)),
1045+
},
1046+
minted_id: Arc::clone(&minted_id),
1047+
release: Arc::clone(&release),
1048+
};
1049+
1050+
let remote_node = node(
1051+
1,
1052+
"remote::open_stream",
1053+
vec![sub_flow_param(100, "on_message", 2)],
1054+
None,
1055+
Some("action.svc"),
1056+
);
1057+
// Reads both action-supplied arguments back independently via their
1058+
// own `input_index`, so the assertion below proves neither slot
1059+
// leaked into or overwrote the other.
1060+
let sub_flow_target = node(
1061+
2,
1062+
"std::number::add",
1063+
vec![
1064+
input_type_param(200, "first", 1, 0, 0),
1065+
input_type_param(201, "second", 1, 0, 1),
1066+
],
1067+
None,
1068+
None,
1069+
);
1070+
1071+
let flow = ExecutionFlow {
1072+
flow_id: 1,
1073+
project_id: 7,
1074+
starting_node_id: 1,
1075+
node_functions: vec![remote_node, sub_flow_target],
1076+
input_value: None,
1077+
};
1078+
1079+
let parent_engine = Arc::clone(&engine);
1080+
let parent = tokio::spawn(async move {
1081+
parent_engine
1082+
.execute_flow_report_async("parent-1", flow, Some(&remote), false)
1083+
.await
1084+
});
1085+
1086+
let execution_identifier = wait_for_minted_id(&minted_id).await;
1087+
1088+
let request = ActionSubFlowExecutionRequest {
1089+
execution_identifier: execution_identifier.clone(),
1090+
parameters: vec![int_value(3), int_value(4)],
1091+
};
1092+
let result = build_sub_flow_execution_result(request, &engine, None, false).await;
1093+
match result.result {
1094+
Some(execution_result::Result::Success(value)) => assert_eq!(value, int_value(7)),
1095+
other => panic!("expected success result, got {:?}", other),
1096+
}
1097+
1098+
release.notify_one();
1099+
parent.await.expect("parent task should not panic");
1100+
}
1101+
10091102
#[tokio::test]
10101103
async fn sub_flow_execution_result_reports_not_found_for_unknown_id() {
10111104
let engine = ExecutionEngine::new();

0 commit comments

Comments
 (0)