From 7ff74043de5aec9c8b9c2e0087bfc0693d3beb8d Mon Sep 17 00:00:00 2001 From: SunSunSun689 Date: Fri, 31 Jul 2026 15:13:07 +0800 Subject: [PATCH 1/2] fix(core): validate nested module required inputs --- libraries/core/src/descriptor/expand.rs | 73 +++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/libraries/core/src/descriptor/expand.rs b/libraries/core/src/descriptor/expand.rs index 87900ac758..1641f3cd06 100644 --- a/libraries/core/src/descriptor/expand.rs +++ b/libraries/core/src/descriptor/expand.rs @@ -246,6 +246,12 @@ pub fn check_module_file(module_path: &Path) -> eyre::Result<()> { // // Load nested module to collect its declared outputs. let nested_module = load_module_file(&nested_canonical)?; + check_nested_module_required_inputs( + &module_file.module.name, + &node.id, + &nested_module.module, + &node.inputs, + )?; for output in &nested_module.module.outputs { inner_outputs.insert(output.to_string()); } @@ -266,6 +272,27 @@ pub fn check_module_file(module_path: &Path) -> eyre::Result<()> { Ok(()) } +fn check_nested_module_required_inputs( + module_name: &str, + node_id: &NodeId, + nested_module: &ModuleHeader, + node_inputs: &BTreeMap, +) -> eyre::Result<()> { + for declared_input in &nested_module.inputs { + if !node_inputs.contains_key(declared_input) { + bail!( + "module `{}`: nested module `{}` declares required input `{}` \ + but node `{}` does not provide it", + module_name, + nested_module.name, + declared_input, + node_id, + ); + } + } + Ok(()) +} + /// Check that every `_mod/X` reference in `inputs` points to a declared /// module input (or optional input). Shared by the node-level `inputs` /// check and the operator/custom `config.inputs` / `run_config.inputs` @@ -1788,6 +1815,52 @@ nodes: assert!(result.unwrap_err().to_string().contains("missing")); } + #[test] + fn check_module_file_rejects_nested_module_missing_required_input() { + let tmp = TempDir::new().unwrap(); + + write_file( + tmp.path(), + "leaf.yml", + r#" +module: + name: leaf + inputs: [data] + outputs: [out] + +nodes: + - id: worker + path: worker.py + inputs: + x: _mod/data + outputs: + - out +"#, + ); + + let path = write_file( + tmp.path(), + "outer.yml", + r#" +module: + name: outer + inputs: [] + outputs: [out] + +nodes: + - id: nested + module: leaf.yml +"#, + ); + + let result = check_module_file(&path); + assert!(result.is_err()); + let msg = result.unwrap_err().to_string(); + assert!(msg.contains("leaf"), "got: {msg}"); + assert!(msg.contains("data"), "got: {msg}"); + assert!(msg.contains("nested"), "got: {msg}"); + } + /// Regression test for #2851: `check_module_file` must accept a nested /// module reference that points to a sibling directory inside the same /// project (e.g. `../shared/base.yml`). The real expansion path From f3662304a1359140ca30a9ca7c76bfa7036f216c Mon Sep 17 00:00:00 2001 From: SunSunSun689 Date: Thu, 13 Aug 2026 15:27:01 +0800 Subject: [PATCH 2/2] fix: restore nested module input validator --- libraries/core/src/descriptor/expand.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/libraries/core/src/descriptor/expand.rs b/libraries/core/src/descriptor/expand.rs index 00c42e539c..43cb3f0284 100644 --- a/libraries/core/src/descriptor/expand.rs +++ b/libraries/core/src/descriptor/expand.rs @@ -290,6 +290,27 @@ fn reject_duplicate_ports(module_name: &str, field: &str, ports: &[DataId]) -> e Ok(()) } +fn check_nested_module_required_inputs( + module_name: &str, + node_id: &NodeId, + nested_module: &ModuleHeader, + node_inputs: &BTreeMap, +) -> eyre::Result<()> { + for declared_input in &nested_module.inputs { + if !node_inputs.contains_key(declared_input) { + bail!( + "module `{}`: nested module `{}` declares required input `{}` \ + but node `{}` does not provide it", + module_name, + nested_module.name, + declared_input, + node_id, + ); + } + } + Ok(()) +} + /// Check that every `_mod/X` reference in `inputs` points to a declared /// module input (or optional input). Shared by the node-level `inputs` /// check and the operator/custom `config.inputs` / `run_config.inputs`