From 94a00839e1d157d6e6c4aa7cd28e10ddded3dd8e Mon Sep 17 00:00:00 2001 From: jawwad-ali Date: Wed, 22 Jul 2026 10:21:57 +0500 Subject: [PATCH] fix(workflows): command/prompt steps fail cleanly on a non-string integration _try_dispatch guarded only 'if not integration_key', then called get_integration(integration_key). A non-string integration (a list/dict, or an expression like integration: "{{ steps.pick.output.agents }}" that resolves to a list) reached the registry dict lookup and raised 'TypeError: unhashable type: list', aborting the entire workflow run. Widen the guard to also require a str, so a non-string integration is treated as not-dispatchable and execute() falls through to its existing FAILED StepResult (unconfigured integration=None still returns None as before). Applied to both command and prompt steps. Tests: a list integration now yields a FAILED result (fail before: TypeError). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workflows/steps/command/__init__.py | 6 +++- .../workflows/steps/prompt/__init__.py | 5 +++- tests/test_workflows.py | 29 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/specify_cli/workflows/steps/command/__init__.py b/src/specify_cli/workflows/steps/command/__init__.py index e28f305172..99bbf76e69 100644 --- a/src/specify_cli/workflows/steps/command/__init__.py +++ b/src/specify_cli/workflows/steps/command/__init__.py @@ -153,7 +153,11 @@ def _try_dispatch( not possible (integration not found, CLI not installed, or dispatch not supported). """ - if not integration_key: + if not integration_key or not isinstance(integration_key, str): + # A non-string integration (a list/dict/expression that resolved to + # one) would raise TypeError: unhashable type from get_integration's + # dict lookup below and abort the whole run. Treat it as "not + # dispatchable" so execute() falls through to its FAILED StepResult. return None try: diff --git a/src/specify_cli/workflows/steps/prompt/__init__.py b/src/specify_cli/workflows/steps/prompt/__init__.py index c8f8a32fd6..d60cca451a 100644 --- a/src/specify_cli/workflows/steps/prompt/__init__.py +++ b/src/specify_cli/workflows/steps/prompt/__init__.py @@ -102,7 +102,10 @@ def _try_dispatch( context: StepContext, ) -> dict[str, Any] | None: """Dispatch *prompt* directly through the integration CLI.""" - if not integration_key or not prompt: + if not integration_key or not isinstance(integration_key, str) or not prompt: + # A non-string integration would raise TypeError: unhashable type + # from get_integration's dict lookup and abort the run; treat it as + # not dispatchable so execute() falls through to its FAILED result. return None try: diff --git a/tests/test_workflows.py b/tests/test_workflows.py index cd1b235411..60cd1c7329 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -1078,6 +1078,21 @@ def test_step_override_integration(self): result = step.execute(config, ctx) assert result.output["integration"] == "gemini" + def test_execute_non_string_integration_fails_cleanly(self): + """A non-string integration (e.g. a list from an expression that resolved + to one) must FAIL the step cleanly, not crash the run with + 'TypeError: unhashable type: list' from get_integration's dict lookup.""" + from specify_cli.workflows.steps.command import CommandStep + from specify_cli.workflows.base import StepContext, StepStatus + + step = CommandStep() + config = { + "id": "s", "command": "speckit.plan", + "integration": ["claude"], "input": {}, + } + result = step.execute(config, StepContext()) + assert result.status == StepStatus.FAILED + def test_step_override_model(self): from unittest.mock import patch from specify_cli.workflows.steps.command import CommandStep @@ -1275,6 +1290,20 @@ def test_execute_basic(self): assert result.output["integration"] == "claude" assert result.output["dispatched"] is False + def test_execute_non_string_integration_fails_cleanly(self): + """A non-string integration must FAIL the step cleanly, not crash with + 'TypeError: unhashable type: list' from get_integration's dict lookup.""" + from specify_cli.workflows.steps.prompt import PromptStep + from specify_cli.workflows.base import StepContext, StepStatus + + step = PromptStep() + config = { + "id": "p", "type": "prompt", "prompt": "do it", + "integration": ["claude"], + } + result = step.execute(config, StepContext()) + assert result.status == StepStatus.FAILED + def test_execute_with_step_integration(self): from unittest.mock import patch from specify_cli.workflows.steps.prompt import PromptStep