mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 11:41:04 +00:00
* Codex worktree snapshot: startup-cleanup Co-authored-by: Codex * Add Python SDK real smoke test Adds a repository-only real E2E smoke script for the Python SDK, plus npm and developer documentation entry points. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address review findings — bugs, type safety, and test coverage - Fix prepare_spawn_info: JS files now use "node" instead of sys.executable - Fix protocol.py: correct total=False misuse on 7 TypedDicts (required fields were optional) - Fix query.py: add _closed guard in _ensure_started, suppress exceptions in close() - Fix sync_query.py: prevent close() deadlock, add context manager, add timeouts - Fix transport.py: handle malformed JSON lines, add _closed guard in start() - Fix validation.py: use uuid.RFC_4122 instead of magic UUID - Fix __init__.py: export TextBlock, widen query_sync signature - Remove dead code: ensure_not_aborted, write_json_line, _thread_error - Add 12 new tests (29 → 41): context managers, JSON skip, closed guards, spawn info, timeouts Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address wenshao review — session_id, bool validation, debug stderr - Fix continue_session=True generating a wrong random session_id - Add _as_optional_bool helper for strict type validation on bool fields - Default debug stderr to sys.stderr when no custom callback is provided Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): address remaining wenshao review feedback Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test(cli): harden settings dialog restart prompt test Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): review fixes — UUID compat, stderr fallback, sync cleanup - Remove UUID version restriction to support v6/v7/v8 (RFC 9562) - Always write to sys.stderr when stderr callback raises (was silent when debug=False) - Prevent duplicate _STOP sentinel in SyncQuery.close() via _stop_sent flag - Add ruff format --check to CI workflow - Fix smoke_real.py version guard: fail early before imports instead of NameError - Apply ruff format to existing files Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): remaining review fixes — exit_code attr, guard strictness, sync timeout - Add exit_code attribute to ProcessExitError for programmatic access - Strengthen is_control_response/is_control_cancel guards to require payload fields, preventing misrouting of malformed messages - Expose control_request_timeout property on Query so SyncQuery uses the configured timeout instead of a hardcoded 30s default - Use dataclasses.replace() instead of direct mutation on frozen-style QueryOptions in query() factory - Add ResourceWarning in SyncQuery.__del__ when not properly closed Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): add exit_code default and guard __del__ against partial GC - Give ProcessExitError.exit_code a default value (-1) so user code can construct the exception with just a message string - Wrap SyncQuery.__del__ in try/except AttributeError to prevent crashes when the object is partially garbage-collected Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): review fixes — resource leak, type safety, CI matrix, docs - Fix SyncQuery.__del__ to call close() on GC instead of only warning - Replace hasattr duck-type check with isinstance(prompt, AsyncIterable) - Type-validate permission_mode/auth_type in QueryOptions.from_mapping - Use TypeGuard return types on all is_sdk_*/is_control_* predicates - Add 5s margin to sync wrapper timeouts to prevent error type masking - Expand CI matrix to test Python 3.10, 3.11, 3.12 - Change ProcessExitError.exit_code default from -1 to None - Add stderr to docs QueryOptions listing - Update README sync example to use context manager pattern Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): preserve iterator exhaustion state and suppress detached task warning - Add _exhausted flag to Query.__anext__ and SyncQuery.__next__ so repeated iteration after end-of-stream raises Stop(Async)Iteration instead of blocking forever. - Remove re-raise in _initialize() to prevent asyncio "Task exception was never retrieved" warning on detached tasks; the error is already surfaced via _finish_with_error(). Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): reject mcp_servers at validation time and add iterator/init tests - Reject mcp_servers in validate_query_options() with a clear error instead of advertising MCP support to the CLI and then failing at runtime when mcp_message arrives. - Remove dead mcp_servers branch from _initialize(). - Add tests for async/sync iterator exhaustion, detached init task warning suppression, and mcp_servers validation. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(sdk-python): fix ruff lint errors in new tests - Use ControlRequestTimeoutError instead of bare Exception (B017) - Fix import sorting for stdlib vs third-party (I001) - Break long line to stay within 88-char limit (E501) Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * style(sdk-python): apply ruff format to new tests Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
174 lines
5.9 KiB
Python
174 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, cast
|
|
|
|
import pytest
|
|
from qwen_code_sdk.errors import ValidationError
|
|
from qwen_code_sdk.types import QueryOptions, TimeoutOptions
|
|
from qwen_code_sdk.validation import validate_query_options
|
|
|
|
VALID_UUID = "123e4567-e89b-12d3-a456-426614174000"
|
|
|
|
|
|
def test_rejects_resume_with_continue_session() -> None:
|
|
with pytest.raises(ValidationError, match="resume together with continue_session"):
|
|
validate_query_options(
|
|
QueryOptions(
|
|
resume=VALID_UUID,
|
|
continue_session=True,
|
|
)
|
|
)
|
|
|
|
|
|
def test_rejects_session_id_with_resume() -> None:
|
|
with pytest.raises(ValidationError, match="Cannot use session_id with resume"):
|
|
validate_query_options(
|
|
QueryOptions(
|
|
session_id=VALID_UUID,
|
|
resume="223e4567-e89b-12d3-a456-426614174000",
|
|
)
|
|
)
|
|
|
|
|
|
def test_rejects_invalid_session_id() -> None:
|
|
with pytest.raises(ValidationError, match="Invalid session_id"):
|
|
validate_query_options(QueryOptions(session_id="not-a-uuid"))
|
|
|
|
|
|
def test_rejects_invalid_resume() -> None:
|
|
with pytest.raises(ValidationError, match="Invalid resume"):
|
|
validate_query_options(QueryOptions(resume="not-a-uuid"))
|
|
|
|
|
|
def test_rejects_invalid_permission_mode() -> None:
|
|
with pytest.raises(ValidationError, match="Invalid permission_mode"):
|
|
validate_query_options(
|
|
QueryOptions.from_mapping({"permission_mode": "unsafe-mode"})
|
|
)
|
|
|
|
|
|
def test_rejects_invalid_auth_type() -> None:
|
|
with pytest.raises(ValidationError, match="Invalid auth_type"):
|
|
validate_query_options(QueryOptions.from_mapping({"auth_type": "custom"}))
|
|
|
|
|
|
def test_from_mapping_rejects_non_callable_can_use_tool() -> None:
|
|
with pytest.raises(TypeError, match="can_use_tool must be callable"):
|
|
QueryOptions.from_mapping({"can_use_tool": "bad"})
|
|
|
|
|
|
def test_from_mapping_rejects_non_callable_stderr() -> None:
|
|
with pytest.raises(TypeError, match="stderr must be callable"):
|
|
QueryOptions.from_mapping({"stderr": "bad"})
|
|
|
|
|
|
def test_validation_rejects_non_callable_can_use_tool() -> None:
|
|
with pytest.raises(ValidationError, match="can_use_tool must be callable"):
|
|
validate_query_options(QueryOptions(can_use_tool=cast(Any, "bad")))
|
|
|
|
|
|
def test_validation_rejects_non_callable_stderr() -> None:
|
|
with pytest.raises(ValidationError, match="stderr must be callable"):
|
|
validate_query_options(QueryOptions(stderr=cast(Any, "bad")))
|
|
|
|
|
|
def test_from_mapping_rejects_sync_can_use_tool() -> None:
|
|
def can_use_tool( # type: ignore[no-untyped-def]
|
|
tool_name, tool_input, context
|
|
):
|
|
return {"behavior": "deny", "message": "bad"}
|
|
|
|
with pytest.raises(TypeError, match="can_use_tool must be an async callable"):
|
|
QueryOptions.from_mapping({"can_use_tool": can_use_tool})
|
|
|
|
|
|
def test_validation_rejects_sync_can_use_tool() -> None:
|
|
def can_use_tool( # type: ignore[no-untyped-def]
|
|
tool_name, tool_input, context
|
|
):
|
|
return {"behavior": "deny", "message": "bad"}
|
|
|
|
with pytest.raises(ValidationError, match="can_use_tool must be an async callable"):
|
|
validate_query_options(QueryOptions(can_use_tool=cast(Any, can_use_tool)))
|
|
|
|
|
|
def test_from_mapping_rejects_can_use_tool_with_wrong_arity() -> None:
|
|
async def can_use_tool(
|
|
tool_name: str,
|
|
tool_input: dict[str, Any],
|
|
) -> dict[str, str]:
|
|
return {"behavior": "deny"}
|
|
|
|
with pytest.raises(
|
|
TypeError, match="can_use_tool must accept exactly 3 positional arguments"
|
|
):
|
|
QueryOptions.from_mapping({"can_use_tool": can_use_tool})
|
|
|
|
|
|
def test_validation_rejects_can_use_tool_with_wrong_arity() -> None:
|
|
async def can_use_tool(
|
|
tool_name: str,
|
|
tool_input: dict[str, Any],
|
|
) -> dict[str, str]:
|
|
return {"behavior": "deny"}
|
|
|
|
with pytest.raises(
|
|
ValidationError,
|
|
match="can_use_tool must accept exactly 3 positional arguments",
|
|
):
|
|
validate_query_options(QueryOptions(can_use_tool=cast(Any, can_use_tool)))
|
|
|
|
|
|
def test_from_mapping_rejects_stderr_with_wrong_arity() -> None:
|
|
def stderr() -> None:
|
|
return None
|
|
|
|
with pytest.raises(
|
|
TypeError, match="stderr must accept exactly 1 positional argument"
|
|
):
|
|
QueryOptions.from_mapping({"stderr": stderr})
|
|
|
|
|
|
def test_validation_rejects_stderr_with_wrong_arity() -> None:
|
|
def stderr() -> None:
|
|
return None
|
|
|
|
with pytest.raises(
|
|
ValidationError, match="stderr must accept exactly 1 positional argument"
|
|
):
|
|
validate_query_options(QueryOptions(stderr=cast(Any, stderr)))
|
|
|
|
|
|
def test_rejects_invalid_max_session_turns() -> None:
|
|
with pytest.raises(ValidationError, match="max_session_turns"):
|
|
validate_query_options(QueryOptions(max_session_turns=-2))
|
|
|
|
|
|
def test_rejects_empty_qwen_executable_path() -> None:
|
|
with pytest.raises(
|
|
ValidationError, match="path_to_qwen_executable cannot be empty"
|
|
):
|
|
validate_query_options(QueryOptions(path_to_qwen_executable=" "))
|
|
|
|
|
|
def test_timeout_rejects_non_numeric_value() -> None:
|
|
with pytest.raises(TypeError, match=r"timeout\.can_use_tool must be a positive"):
|
|
TimeoutOptions.from_mapping({"can_use_tool": "fast"})
|
|
|
|
|
|
def test_timeout_rejects_negative_value() -> None:
|
|
pattern = r"timeout\.control_request must be a positive"
|
|
with pytest.raises(ValueError, match=pattern):
|
|
TimeoutOptions.from_mapping({"control_request": -1})
|
|
|
|
|
|
def test_timeout_rejects_boolean_value() -> None:
|
|
with pytest.raises(TypeError, match=r"timeout\.stream_close must be a positive"):
|
|
TimeoutOptions.from_mapping({"stream_close": True})
|
|
|
|
|
|
def test_rejects_mcp_servers() -> None:
|
|
with pytest.raises(ValidationError, match="mcp_servers is not supported"):
|
|
validate_query_options(
|
|
QueryOptions(mcp_servers={"my-server": {"command": "node", "args": []}})
|
|
)
|