From d6e09a17c47733dc0ec627ca447632ca35c66d22 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Viet <123613986+NgoQuocViet2001@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:18:25 +0700 Subject: [PATCH] fix(workflows): validate non-string step types (#4111) Return an actionable validation error when a workflow step type is a YAML list or mapping instead of raising during registry membership checks. Assisted-by: OpenAI Codex (model: GPT-5, autonomous) --- src/specify_cli/workflows/engine.py | 11 +++++++++++ tests/test_workflows.py | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/specify_cli/workflows/engine.py b/src/specify_cli/workflows/engine.py index b9bf8370..a74450ed 100644 --- a/src/specify_cli/workflows/engine.py +++ b/src/specify_cli/workflows/engine.py @@ -366,6 +366,17 @@ def _validate_steps( # Determine step type step_type = step_config.get("type", "command") + if not isinstance(step_type, str): + # Registry keys are strings. Checking an unhashable YAML value + # (for example ``type: [shell]`` or a mapping) against the set + # below raises a raw TypeError before validation can report the + # authoring mistake. Guard every non-string shape first, matching + # the typed validation already applied to workflow and step IDs. + errors.append( + f"Step {step_id!r}: 'type' must be a string, got " + f"{type(step_type).__name__} ({step_type!r})." + ) + continue if step_type not in _get_valid_step_types(): errors.append( f"Step {step_id!r} has invalid type {step_type!r}." diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 95baf22d..afd70ade 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -4565,6 +4565,29 @@ steps: errors = validate_workflow(definition) assert any("invalid type" in e.lower() for e in errors) + @pytest.mark.parametrize("step_type", [["shell"], {"name": "shell"}]) + def test_non_string_step_type_reports_error(self, step_type): + """Unhashable YAML values must not crash registry membership checks.""" + from specify_cli.workflows.engine import WorkflowDefinition, validate_workflow + + definition = WorkflowDefinition( + { + "workflow": { + "id": "test", + "name": "Test", + "version": "1.0.0", + }, + "steps": [{"id": "bad", "type": step_type}], + } + ) + + errors = validate_workflow(definition) + + assert errors == [ + f"Step 'bad': 'type' must be a string, got " + f"{type(step_type).__name__} ({step_type!r})." + ] + def test_nested_step_validation(self): from specify_cli.workflows.engine import WorkflowDefinition, validate_workflow