From d2ecdf0857c0926c581d138c0831c83233ad990f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 24 Sep 2026 19:22:30 +0000 Subject: [PATCH] Fix version assertions and reject non-object Cursor tool payloads Version tests now compare against the VERSION file so a bump does not leave them stale. A non-object mcpToolCall or getMcpToolsToolCall fails the run as a malformed Cursor response. Co-authored-by: Dor Ingber --- src/agent_test_kit/client/cursor_trace.py | 16 ++++++++++++---- tests/test_cursor_trace_extra.py | 18 ++++++++++++++++++ tests/test_version.py | 8 ++++++-- 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/agent_test_kit/client/cursor_trace.py b/src/agent_test_kit/client/cursor_trace.py index c339fcd..c71a5cc 100755 --- a/src/agent_test_kit/client/cursor_trace.py +++ b/src/agent_test_kit/client/cursor_trace.py @@ -48,6 +48,13 @@ def _first_value(source: dict[str, Any], *keys: str) -> Any | None: return None +def _object_payload(value: Any, label: str) -> dict[str, Any]: + if not isinstance(value, dict): + msg = f"{label} must be an object" + raise ValueError(msg) + return value + + def _tool_payload(event: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]: tool_call = event.get("tool_call") if tool_call is not None and not isinstance(tool_call, dict): @@ -56,11 +63,12 @@ def _tool_payload(event: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any] normalized_tool_call = tool_call or {} mcp: dict[str, Any] = {} if "mcpToolCall" in normalized_tool_call: - raw = normalized_tool_call.get("mcpToolCall") - mcp = raw if isinstance(raw, dict) else {} + mcp = _object_payload(normalized_tool_call.get("mcpToolCall"), "mcpToolCall") elif "getMcpToolsToolCall" in normalized_tool_call: - raw = normalized_tool_call.get("getMcpToolsToolCall") - mcp = raw if isinstance(raw, dict) else {} + mcp = _object_payload( + normalized_tool_call.get("getMcpToolsToolCall"), + "getMcpToolsToolCall", + ) else: for key, value in normalized_tool_call.items(): if key.endswith("ToolCall") and isinstance(value, dict): diff --git a/tests/test_cursor_trace_extra.py b/tests/test_cursor_trace_extra.py index 93e1ecd..7a6389f 100755 --- a/tests/test_cursor_trace_extra.py +++ b/tests/test_cursor_trace_extra.py @@ -265,6 +265,24 @@ def test_stream_result_extracts_explicit_total_tokens( assert trace.token_usage == expected +@pytest.mark.agent_unit +@pytest.mark.parametrize( + "payload_key", + ["mcpToolCall", "getMcpToolsToolCall"], +) +def test_nested_tool_call_payload_must_be_object(payload_key: str) -> None: + event = json.dumps( + { + "type": "tool_call", + "subtype": "called", + "tool_call": {payload_key: ["invalid"]}, + } + ) + + with pytest.raises(ValueError, match=rf"{payload_key} must be an object"): + parse_stream_json_stdout(event) + + @pytest.mark.agent_unit @pytest.mark.parametrize("field", ["tool_calls", "events"]) def test_nested_trace_collections_must_be_lists(field: str) -> None: diff --git a/tests/test_version.py b/tests/test_version.py index f993e74..c943cdc 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -2,9 +2,13 @@ from __future__ import annotations +from pathlib import Path + import agent_test_kit from agent_test_kit._version import _read_version_file, _resolve_version +_VERSION_FILE = Path(__file__).resolve().parents[1] / "VERSION" + def test_version_is_semver_like() -> None: version = agent_test_kit.__version__ @@ -14,8 +18,8 @@ def test_version_is_semver_like() -> None: def test_read_version_file() -> None: - assert _read_version_file() == "0.1.1" + assert _read_version_file() == _VERSION_FILE.read_text(encoding="utf-8").strip() def test_resolve_version_matches_file() -> None: - assert _resolve_version() == "0.1.1" + assert _resolve_version() == _read_version_file()