From 6a20e187ddfebeb97f87e968a6281e19113cea23 Mon Sep 17 00:00:00 2001 From: Teknium Date: Thu, 23 Apr 2026 16:19:17 -0700 Subject: [PATCH] test,chore: cover stringified array/object coercion + AUTHOR_MAP entry Follow-up to the cherry-picked coercion commit: adds 9 regression tests covering array/object parsing, invalid-JSON passthrough, wrong-shape preservation, and the issue #3947 gmail-mcp scenario end-to-end. Adds dan@danlynn.com -> danklynn to scripts/release.py AUTHOR_MAP so the salvage PR's contributor attribution doesn't break CI. --- scripts/release.py | 1 + tests/run_agent/test_tool_arg_coercion.py | 51 +++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/scripts/release.py b/scripts/release.py index 5a38adc4f..2a9169a5f 100755 --- a/scripts/release.py +++ b/scripts/release.py @@ -167,6 +167,7 @@ AUTHOR_MAP = { "socrates1024@gmail.com": "socrates1024", "seanalt555@gmail.com": "Salt-555", "satelerd@gmail.com": "satelerd", + "dan@danlynn.com": "danklynn", "numman.ali@gmail.com": "nummanali", "rohithsaimidigudla@gmail.com": "whitehatjr1001", "0xNyk@users.noreply.github.com": "0xNyk", diff --git a/tests/run_agent/test_tool_arg_coercion.py b/tests/run_agent/test_tool_arg_coercion.py index cf1876d4e..bc84b2bf6 100644 --- a/tests/run_agent/test_tool_arg_coercion.py +++ b/tests/run_agent/test_tool_arg_coercion.py @@ -134,6 +134,31 @@ class TestCoerceValue: """A non-numeric string in [number, string] should stay a string.""" assert _coerce_value("hello", ["number", "string"]) == "hello" + def test_array_type_parsed_from_json_string(self): + """Stringified JSON arrays are parsed into native lists.""" + assert _coerce_value('["a", "b"]', "array") == ["a", "b"] + assert _coerce_value("[1, 2, 3]", "array") == [1, 2, 3] + + def test_object_type_parsed_from_json_string(self): + """Stringified JSON objects are parsed into native dicts.""" + assert _coerce_value('{"k": "v"}', "object") == {"k": "v"} + assert _coerce_value('{"n": 1}', "object") == {"n": 1} + + def test_array_invalid_json_preserved(self): + """Unparseable strings are returned unchanged.""" + assert _coerce_value("not-json", "array") == "not-json" + + def test_object_invalid_json_preserved(self): + assert _coerce_value("not-json", "object") == "not-json" + + def test_array_type_wrong_shape_preserved(self): + """A JSON object passed for an 'array' slot is preserved as a string.""" + assert _coerce_value('{"k": "v"}', "array") == '{"k": "v"}' + + def test_object_type_wrong_shape_preserved(self): + """A JSON array passed for an 'object' slot is preserved as a string.""" + assert _coerce_value('["a"]', "object") == '["a"]' + # ── Full coerce_tool_args with registry ─────────────────────────────────── @@ -212,6 +237,32 @@ class TestCoerceToolArgs: assert result["items"] == [1, 2, 3] assert result["config"] == {"key": "val"} + def test_coerces_stringified_array_arg(self): + """Regression for #3947 — MCP servers using z.array() expect lists, not strings.""" + schema = self._mock_schema({ + "messageIds": {"type": "array", "items": {"type": "string"}}, + }) + with patch("model_tools.registry.get_schema", return_value=schema): + args = {"messageIds": '["abc", "def"]'} + result = coerce_tool_args("test_tool", args) + assert result["messageIds"] == ["abc", "def"] + + def test_coerces_stringified_object_arg(self): + """Stringified JSON objects get parsed into dicts.""" + schema = self._mock_schema({"config": {"type": "object"}}) + with patch("model_tools.registry.get_schema", return_value=schema): + args = {"config": '{"max": 50}'} + result = coerce_tool_args("test_tool", args) + assert result["config"] == {"max": 50} + + def test_invalid_json_array_preserved_as_string(self): + """If the string isn't valid JSON, pass it through — let the tool decide.""" + schema = self._mock_schema({"items": {"type": "array"}}) + with patch("model_tools.registry.get_schema", return_value=schema): + args = {"items": "not-json"} + result = coerce_tool_args("test_tool", args) + assert result["items"] == "not-json" + def test_extra_args_without_schema_left_alone(self): """Args not in the schema properties are not touched.""" schema = self._mock_schema({"limit": {"type": "integer"}})