diff --git a/packages/sdk-python/src/qwen_code_sdk/validation.py b/packages/sdk-python/src/qwen_code_sdk/validation.py index 5e144da4d1e..d52dc9c7942 100644 --- a/packages/sdk-python/src/qwen_code_sdk/validation.py +++ b/packages/sdk-python/src/qwen_code_sdk/validation.py @@ -135,9 +135,7 @@ def validate_query_options(options: QueryOptions) -> None: ) if options.max_session_turns is not None and ( - isinstance(options.max_session_turns, bool) - or not isinstance(options.max_session_turns, int) - or options.max_session_turns < -1 + not _is_int(options.max_session_turns) or options.max_session_turns < -1 ): raise ValidationError("max_session_turns must be -1 or a non-negative integer") @@ -147,13 +145,16 @@ def validate_query_options(options: QueryOptions) -> None: ): raise ValidationError("path_to_qwen_executable cannot be empty") - if options.max_tool_calls is not None and options.max_tool_calls < -1: + if options.max_tool_calls is not None and ( + not _is_int(options.max_tool_calls) or options.max_tool_calls < -1 + ): raise ValidationError("max_tool_calls must be -1 or a non-negative integer") - if options.max_subagent_depth is not None and not ( - 1 <= options.max_subagent_depth <= 100 + if options.max_subagent_depth is not None and ( + not _is_int(options.max_subagent_depth) + or not (1 <= options.max_subagent_depth <= 100) ): - raise ValidationError("max_subagent_depth must be between 1 and 100") + raise ValidationError("max_subagent_depth must be an integer between 1 and 100") if options.agents: for i, agent in enumerate(options.agents): @@ -205,6 +206,15 @@ def validate_query_options(options: QueryOptions) -> None: raise ValidationError("proxy cannot be empty") +def _is_int(value: object) -> bool: + """True for a real integer. + + ``bool`` subclasses ``int``, so ``isinstance(True, int)`` is True and a + bare isinstance check would let ``max_tool_calls=True`` through as 1. + """ + return isinstance(value, int) and not isinstance(value, bool) + + def _validate_optional_callable( value: object, validator: Callable[[object, type[ValidationError]], None], diff --git a/packages/sdk-python/tests/unit/test_validation.py b/packages/sdk-python/tests/unit/test_validation.py index 10bdfd1d05c..25ad4cf65fa 100644 --- a/packages/sdk-python/tests/unit/test_validation.py +++ b/packages/sdk-python/tests/unit/test_validation.py @@ -188,6 +188,31 @@ def test_rejects_invalid_max_subagent_depth() -> None: validate_query_options(QueryOptions(max_subagent_depth=0)) +@pytest.mark.parametrize("value", [True, False, 0.5]) +def test_rejects_non_integer_max_tool_calls(value: object) -> None: + # The error message promises "-1 or a non-negative integer", and the value + # is stringified straight onto the CLI, so `True` would become + # `--max-tool-calls True`. + with pytest.raises(ValidationError, match="max_tool_calls"): + validate_query_options(QueryOptions(max_tool_calls=cast(Any, value))) + + +@pytest.mark.parametrize("value", [True, False, 2.5]) +def test_rejects_non_integer_max_subagent_depth(value: object) -> None: + with pytest.raises(ValidationError, match="max_subagent_depth"): + validate_query_options(QueryOptions(max_subagent_depth=cast(Any, value))) + + +def test_accepts_valid_integer_limits() -> None: + # The in-range integers these options are documented to take must survive. + validate_query_options( + QueryOptions(max_tool_calls=-1, max_session_turns=-1, max_subagent_depth=1) + ) + validate_query_options( + QueryOptions(max_tool_calls=0, max_session_turns=0, max_subagent_depth=100) + ) + + def test_rejects_agents_missing_required_fields() -> None: with pytest.raises(ValidationError, match="missing required field"): validate_query_options(QueryOptions(agents=[{"name": "test"}]))