Skip to content

Commit aee9df0

Browse files
jawwad-aliclaude
andauthored
fix(workflows): command/prompt steps fail cleanly on a non-string integration (#3626)
_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) <noreply@anthropic.com>
1 parent 4f4d19b commit aee9df0

3 files changed

Lines changed: 38 additions & 2 deletions

File tree

src/specify_cli/workflows/steps/command/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,11 @@ def _try_dispatch(
189189
not possible (integration not found, CLI not installed, or
190190
dispatch not supported).
191191
"""
192-
if not integration_key:
192+
if not integration_key or not isinstance(integration_key, str):
193+
# A non-string integration (a list/dict/expression that resolved to
194+
# one) would raise TypeError: unhashable type from get_integration's
195+
# dict lookup below and abort the whole run. Treat it as "not
196+
# dispatchable" so execute() falls through to its FAILED StepResult.
193197
return None
194198

195199
try:

src/specify_cli/workflows/steps/prompt/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,10 @@ def _try_dispatch(
138138
context: StepContext,
139139
) -> dict[str, Any] | None:
140140
"""Dispatch *prompt* directly through the integration CLI."""
141-
if not integration_key or not prompt:
141+
if not integration_key or not isinstance(integration_key, str) or not prompt:
142+
# A non-string integration would raise TypeError: unhashable type
143+
# from get_integration's dict lookup and abort the run; treat it as
144+
# not dispatchable so execute() falls through to its FAILED result.
142145
return None
143146

144147
try:

tests/test_workflows.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,6 +1162,21 @@ def test_step_override_integration(self):
11621162
result = step.execute(config, ctx)
11631163
assert result.output["integration"] == "gemini"
11641164

1165+
def test_execute_non_string_integration_fails_cleanly(self):
1166+
"""A non-string integration (e.g. a list from an expression that resolved
1167+
to one) must FAIL the step cleanly, not crash the run with
1168+
'TypeError: unhashable type: list' from get_integration's dict lookup."""
1169+
from specify_cli.workflows.steps.command import CommandStep
1170+
from specify_cli.workflows.base import StepContext, StepStatus
1171+
1172+
step = CommandStep()
1173+
config = {
1174+
"id": "s", "command": "speckit.plan",
1175+
"integration": ["claude"], "input": {},
1176+
}
1177+
result = step.execute(config, StepContext())
1178+
assert result.status == StepStatus.FAILED
1179+
11651180
def test_step_override_model(self):
11661181
from unittest.mock import patch
11671182
from specify_cli.workflows.steps.command import CommandStep
@@ -1359,6 +1374,20 @@ def test_execute_basic(self):
13591374
assert result.output["integration"] == "claude"
13601375
assert result.output["dispatched"] is False
13611376

1377+
def test_execute_non_string_integration_fails_cleanly(self):
1378+
"""A non-string integration must FAIL the step cleanly, not crash with
1379+
'TypeError: unhashable type: list' from get_integration's dict lookup."""
1380+
from specify_cli.workflows.steps.prompt import PromptStep
1381+
from specify_cli.workflows.base import StepContext, StepStatus
1382+
1383+
step = PromptStep()
1384+
config = {
1385+
"id": "p", "type": "prompt", "prompt": "do it",
1386+
"integration": ["claude"],
1387+
}
1388+
result = step.execute(config, StepContext())
1389+
assert result.status == StepStatus.FAILED
1390+
13621391
def test_execute_with_step_integration(self):
13631392
from unittest.mock import patch
13641393
from specify_cli.workflows.steps.prompt import PromptStep

0 commit comments

Comments
 (0)