diff --git a/src/specify_cli/workflows/steps/command/__init__.py b/src/specify_cli/workflows/steps/command/__init__.py index 8d7ea1503..8ab777089 100644 --- a/src/specify_cli/workflows/steps/command/__init__.py +++ b/src/specify_cli/workflows/steps/command/__init__.py @@ -189,7 +189,11 @@ class CommandStep(StepBase): 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 e81ca175d..ed92e1eea 100644 --- a/src/specify_cli/workflows/steps/prompt/__init__.py +++ b/src/specify_cli/workflows/steps/prompt/__init__.py @@ -138,7 +138,10 @@ class PromptStep(StepBase): 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 6ea7e9c31..c51df1934 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -1162,6 +1162,21 @@ class TestCommandStep: 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 @@ -1359,6 +1374,20 @@ class TestPromptStep: 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