diff --git a/src/specify_cli/workflows/steps/prompt/__init__.py b/src/specify_cli/workflows/steps/prompt/__init__.py index 5ec99b794d..c8f8a32fd6 100644 --- a/src/specify_cli/workflows/steps/prompt/__init__.py +++ b/src/specify_cli/workflows/steps/prompt/__init__.py @@ -160,4 +160,16 @@ def validate(self, config: dict[str, Any]) -> list[str]: errors.append( f"Prompt step {config.get('id', '?')!r} is missing 'prompt' field." ) + elif not isinstance(config["prompt"], str): + # execute() str()-coerces prompt and dispatches it to the + # integration CLI, so a null or list 'prompt' would send the Python + # repr ('None', "['review', 'this']") to the model as instructions — + # silently wrong, with no error. Reject non-strings at validation, + # mirroring the shell-step 'run' and command-step input/options type + # checks. An expression like "{{ ... }}" is still a str, so it stays + # valid. + errors.append( + f"Prompt step {config.get('id', '?')!r}: 'prompt' must be a " + f"string, got {type(config['prompt']).__name__}." + ) return errors diff --git a/tests/test_workflows.py b/tests/test_workflows.py index 8fefad740e..cf95fa5b7a 100644 --- a/tests/test_workflows.py +++ b/tests/test_workflows.py @@ -1336,6 +1336,21 @@ def test_validate_missing_prompt(self): errors = step.validate({"id": "test"}) assert any("missing 'prompt'" in e for e in errors) + @pytest.mark.parametrize("bad_prompt", [None, ["review", "this"], 42, {"a": 1}]) + def test_validate_rejects_non_string_prompt(self, bad_prompt): + """A non-string 'prompt' must be rejected at validation. + + execute() str()-coerces prompt and dispatches it to the integration + CLI, so a null or list prompt would otherwise send the Python repr to + the model as instructions — silently wrong. Mirrors the shell-step + 'run' type check. + """ + from specify_cli.workflows.steps.prompt import PromptStep + + step = PromptStep() + errors = step.validate({"id": "p", "prompt": bad_prompt}) + assert any("'prompt' must be a string" in e for e in errors) + def test_validate_valid(self): from specify_cli.workflows.steps.prompt import PromptStep @@ -1343,6 +1358,16 @@ def test_validate_valid(self): errors = step.validate({"id": "test", "prompt": "do something"}) assert errors == [] + def test_validate_accepts_expression_prompt(self): + """A '{{ ... }}' expression prompt is a str, so it stays valid.""" + from specify_cli.workflows.steps.prompt import PromptStep + + step = PromptStep() + errors = step.validate( + {"id": "p", "prompt": "Review {{ inputs.file }}"} + ) + assert errors == [] + class TestShellStep: """Test the shell step type."""