fix(graph): name the offending node type when a map branch reaches a disallowed node

PR #24 follow-up 2. With validate_before_run off, a user-authored
approval/input/end/map node inside a branch reached a message that
called itself an internal error. Name the node type and point at the
validator setting instead.
This commit is contained in:
Dark-Alex-17 committed 2026-09-11 10:56:27 -06:00
1 parent 2f839b332d
commit 31c33b5a23
1 file changed
+65 -7
+65 -7
View File
@@ -221,13 +221,17 @@ async fn run_chain_step(
let node = step_ctx.graph.get_node(current).ok_or_else(|| { let node = step_ctx.graph.get_node(current).ok_or_else(|| {
anyhow!("map node '{map_id}': sub-branch [{idx}] routed to unknown node '{current}'") anyhow!("map node '{map_id}': sub-branch [{idx}] routed to unknown node '{current}'")
})?; })?;
if !matches!( let disallowed = match &node.node_type {
node.node_type, NodeType::Approval(_) => Some("an approval node"),
NodeType::Llm(_) | NodeType::Agent(_) | NodeType::Rag(_) | NodeType::Script(_) NodeType::Input(_) => Some("an input node"),
) { NodeType::End(_) => Some("an end node"),
NodeType::Map(_) => Some("a map node"),
NodeType::Agent(_) | NodeType::Llm(_) | NodeType::Rag(_) | NodeType::Script(_) => None,
};
if let Some(type_phrase) = disallowed {
bail!( bail!(
"map branch '{current}' has type that cannot run inside a map \ "'{current}' is {type_phrase}; approval/input/end/map nodes cannot run inside a \
(validator should have caught this; internal error)" map branch (enable settings.validate_before_run to catch this at load time)"
); );
} }
@@ -1115,12 +1119,66 @@ nodes:
let chain = error_chain(run_graph(yaml, &ws).await); let chain = error_chain(run_graph(yaml, &ws).await);
assert!( assert!(
chain.contains("map branch 'ask' has type that cannot run inside a map"), chain.contains(
"'ask' is an approval node; approval/input/end/map nodes cannot run inside a \
map branch (enable settings.validate_before_run to catch this at load time)"
),
"{chain}" "{chain}"
); );
assert!(chain.contains("failed at node 'ask' (step 2)"), "{chain}"); assert!(chain.contains("failed at node 'ask' (step 2)"), "{chain}");
} }
#[tokio::test]
async fn map_chain_pre_check_rejects_end_node() {
if !cmd_available("python3") {
eprintln!("skipping: python3 not available");
return;
}
let ws = TestWorkspace::new();
ws.write_py("gate.py", r#"print(json.dumps({}))"#);
let yaml = r#"
name: chain
start: fan_out
settings:
validate_before_run: false
initial_state:
items: [1]
nodes:
fan_out:
type: map
over: "{{items}}"
as: item
branch: gate
collect_into: results
next: done
gate:
type: script
script: gate.py
next: finish
finish:
type: end
output: "done early"
done:
type: end
output: "{{results}}"
"#;
let chain = error_chain(run_graph(yaml, &ws).await);
assert!(
chain.contains(
"'finish' is an end node; approval/input/end/map nodes cannot run inside a \
map branch (enable settings.validate_before_run to catch this at load time)"
),
"{chain}"
);
assert!(
chain.contains("map node 'fan_out': sub-branch [0] failed at node 'finish' (step 2)"),
"{chain}"
);
assert_eq!(chain.matches("sub-branch [").count(), 1, "{chain}");
}
#[tokio::test] #[tokio::test]
async fn map_chain_script_error_routes_to_fallback() { async fn map_chain_script_error_routes_to_fallback() {
if !cmd_available("python3") || !cmd_available("bash") { if !cmd_available("python3") || !cmd_available("bash") {