diff --git a/libraries/core/src/descriptor/expand.rs b/libraries/core/src/descriptor/expand.rs index c7e006211..43cb3f028 100644 --- a/libraries/core/src/descriptor/expand.rs +++ b/libraries/core/src/descriptor/expand.rs @@ -234,6 +234,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()); } @@ -284,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` @@ -2231,6 +2258,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