Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/specify_cli/workflows/steps/command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 4 additions & 1 deletion src/specify_cli/workflows/steps/prompt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
29 changes: 29 additions & 0 deletions tests/test_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down