From ea6843c1feb431d01a6eb1f8be2649ef8ccecb5f Mon Sep 17 00:00:00 2001 From: Ali jawwad <33836051+jawwad-ali@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:52:28 +0500 Subject: [PATCH] fix(workflows): guard non-mapping 'inputs:' block in engine._resolve_inputs (#3696) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit execute()/resume() run UNVALIDATED definitions (load_workflow does not validate). WorkflowDefinition stores `inputs` raw, so a non-mapping `inputs:` block (bare `inputs:` -> None, or `inputs: []`) crashed _resolve_inputs at `for name, input_def in definition.inputs.items()` with AttributeError, aborting the whole run. Return {} when inputs is not a mapping, mirroring validate_workflow's own `isinstance(definition.inputs, dict)` check. Protects both call sites (execute and resume); normal dict resolution is unchanged. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) --- src/specify_cli/workflows/engine.py | 8 ++++++++ tests/test_workflows.py | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/specify_cli/workflows/engine.py b/src/specify_cli/workflows/engine.py index 98005a790..03afaee74 100644 --- a/src/specify_cli/workflows/engine.py +++ b/src/specify_cli/workflows/engine.py @@ -1400,6 +1400,14 @@ class WorkflowEngine: ) -> dict[str, Any]: """Resolve workflow inputs against definitions and provided values.""" resolved: dict[str, Any] = {} + # execute()/resume() accept UNVALIDATED definitions (load_workflow does + # not validate). A non-mapping ``inputs:`` block (bare ``inputs:`` -> + # None, or ``inputs: []``) is stored raw, so iterating ``.items()`` here + # would crash the run with AttributeError. Treat a non-mapping inputs + # block as "no inputs"; validate_workflow reports the malformed shape + # via its own isinstance check. + if not isinstance(definition.inputs, dict): + return {} for name, input_def in definition.inputs.items(): if not isinstance(input_def, dict): continue diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 1c29ab56e..54cea4d77 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -3612,6 +3612,23 @@ class TestWorkflowDefinition: assert definition.id == "test-workflow" assert len(definition.inputs) == 2 + @pytest.mark.parametrize( + "block", + [ + "workflow:\n id: w\n name: W\nsteps: []\ninputs: []\n", # list + "workflow:\n id: w\n name: W\nsteps: []\ninputs:\n", # null + ], + ) + def test_resolve_inputs_tolerates_non_mapping_inputs(self, block): + # execute()/resume() run UNVALIDATED definitions; a non-mapping `inputs:` + # block (list/null) is stored raw and would crash _resolve_inputs at + # `.items()`. It must be treated as "no inputs" instead. + from specify_cli.workflows.engine import WorkflowDefinition, WorkflowEngine + + definition = WorkflowDefinition.from_string(block) + resolved = WorkflowEngine()._resolve_inputs(definition, {}) # must not raise + assert resolved == {} + def test_from_string_invalid(self): from specify_cli.workflows.engine import WorkflowDefinition