From 90023843937594946f6ea3b0a825e7adc4f1d020 Mon Sep 17 00:00:00 2001 From: cotovanu-cristian Date: Fri, 6 Feb 2026 12:01:49 +0200 Subject: [PATCH] feat: enhance tests suite --- tests/agent/models/test_agent.py | 76 ++++++++++++++++++- .../react/test_conversational_prompts.py | 25 ++++++ .../agent/utils/test_load_agent_definition.py | 66 ++++++++++++++++ tests/agent/utils/test_text_tokens.py | 58 ++++++++++++++ 4 files changed, 224 insertions(+), 1 deletion(-) diff --git a/tests/agent/models/test_agent.py b/tests/agent/models/test_agent.py index 63a8a917f..e01596ceb 100644 --- a/tests/agent/models/test_agent.py +++ b/tests/agent/models/test_agent.py @@ -1,5 +1,5 @@ import pytest -from pydantic import TypeAdapter +from pydantic import TypeAdapter, ValidationError from uipath.agent.models.agent import ( AgentBuiltInValidatorGuardrail, @@ -2755,3 +2755,77 @@ def test_is_conversational_false_by_default(self): ) assert config.is_conversational is False + + +class TestAgentDefinitionValidation: + """Negative/validation tests for AgentDefinition.""" + + _VALID_AGENT = { + "name": "test-agent", + "settings": { + "model": "gpt-4o", + "maxTokens": 4096, + "temperature": 0, + "engine": "basic-v1", + }, + "messages": [{"role": "system", "content": "hi"}], + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + } + + @pytest.mark.parametrize( + "omit_key", + ["settings", "messages", "inputSchema", "outputSchema"], + ) + def test_missing_required_field_raises(self, omit_key): + """AgentDefinition rejects payloads missing any required field.""" + data = {k: v for k, v in self._VALID_AGENT.items() if k != omit_key} + with pytest.raises(ValidationError): + TypeAdapter(AgentDefinition).validate_python(data) + + @pytest.mark.parametrize( + "settings_override", + [ + {"maxTokens": 4096, "temperature": 0, "engine": "basic-v1"}, + {"model": "gpt-4o", "maxTokens": 4096, "temperature": 0}, + ], + ids=["missing-model", "missing-engine"], + ) + def test_settings_missing_required_subfield_raises(self, settings_override): + """Settings must include both model and engine.""" + data = {**self._VALID_AGENT, "settings": settings_override} + with pytest.raises(ValidationError): + TypeAdapter(AgentDefinition).validate_python(data) + + def test_unknown_types_normalized_gracefully(self): + """Unknown resource, tool, and guardrail types are wrapped, not rejected.""" + data = { + **self._VALID_AGENT, + "resources": [ + { + "$resourceType": "futuristic", + "name": "future-resource", + "description": "unknown resource type", + }, + { + "$resourceType": "tool", + "type": "FutureToolType", + "name": "future-tool", + "description": "unknown tool type", + "inputSchema": {"type": "object", "properties": {}}, + }, + ], + "guardrails": [ + { + "$guardrailType": "futureGuardrail", + "someField": "someValue", + } + ], + } + config = TypeAdapter(AgentDefinition).validate_python(data) + + assert len(config.resources) == 2 + assert isinstance(config.resources[0], AgentUnknownResourceConfig) + assert isinstance(config.resources[1], AgentUnknownToolResourceConfig) + assert len(config.guardrails) == 1 + assert isinstance(config.guardrails[0], AgentUnknownGuardrail) diff --git a/tests/agent/react/test_conversational_prompts.py b/tests/agent/react/test_conversational_prompts.py index caed3d603..a5b44d397 100644 --- a/tests/agent/react/test_conversational_prompts.py +++ b/tests/agent/react/test_conversational_prompts.py @@ -390,3 +390,28 @@ def test_full_settings_json_format(self): assert json_data["company"] == "Big Corp" assert json_data["country"] == "UK" assert json_data["timezone"] == "Europe/London" + + +class TestSpecialCharacterHandling: + """Test handling of special characters in prompts.""" + + def test_unicode_and_emoji_preserved_in_user_context(self): + """Unicode and emoji characters in user settings are preserved.""" + settings = PromptUserSettings(name="日本太郎 🚀", email="taro@example.jp") + result = _get_user_settings_template(settings) + + assert "USER CONTEXT" in result + assert "日本太郎 🚀" in result + + def test_template_syntax_preserved_in_prompt(self): + """Curly braces, double curlies, and newlines pass through the template.""" + prompt = get_chat_system_prompt( + model="claude-3-sonnet", + system_message="Output {key: value}\nLine 2", + agent_name="Agent {{v2}}", + user_settings=None, + ) + + assert "Output {key: value}" in prompt + assert "You are Agent {{v2}}." in prompt + assert "Line 2" in prompt diff --git a/tests/agent/utils/test_load_agent_definition.py b/tests/agent/utils/test_load_agent_definition.py index 996eaf420..a17e1b0be 100644 --- a/tests/agent/utils/test_load_agent_definition.py +++ b/tests/agent/utils/test_load_agent_definition.py @@ -324,3 +324,69 @@ def test_load_agent_definition_missing_evaluators_directory(self, temp_project_d assert result.id == "test-agent-5" assert result.evaluators is None or len(result.evaluators or []) == 0 assert result.evaluation_sets is None or len(result.evaluation_sets or []) == 0 + + +class TestLoadAgentDefinitionErrors: + """Error path tests for load_agent_definition.""" + + @pytest.fixture + def temp_project_dir(self): + """Create a temporary project directory for testing.""" + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + def test_missing_agent_json_raises(self, temp_project_dir): + """Loading from a directory without agent.json raises FileNotFoundError.""" + with pytest.raises(FileNotFoundError): + load_agent_definition(temp_project_dir) + + def test_malformed_json_in_agent_file_raises(self, temp_project_dir): + """Invalid JSON in agent.json raises json.JSONDecodeError.""" + agent_file = temp_project_dir / "agent.json" + agent_file.write_text("{ not valid json !!!") + + with pytest.raises(json.JSONDecodeError): + load_agent_definition(temp_project_dir) + + @pytest.mark.parametrize( + "subdir,filename", + [ + ("resources", "bad_resource.json"), + ("evaluations/evaluators", "broken.json"), + ], + ids=["malformed-resource", "malformed-evaluator"], + ) + def test_malformed_sidecar_file_is_skipped( + self, temp_project_dir, subdir, filename + ): + """Malformed JSON in resources/ or evaluators/ is skipped, not fatal.""" + agent_data = { + "id": "test-malformed", + "name": "Agent with bad sidecar file", + "version": "1.0.0", + "messages": [{"role": "system", "content": "hi"}], + "inputSchema": {"type": "object", "properties": {}}, + "outputSchema": {"type": "object", "properties": {}}, + "settings": { + "model": "gpt-4o", + "maxTokens": 2048, + "temperature": 0.7, + "engine": "basic-v1", + }, + } + + with open(temp_project_dir / "agent.json", "w") as f: + json.dump(agent_data, f) + + target_dir = temp_project_dir / subdir + target_dir.mkdir(parents=True) + (target_dir / filename).write_text("NOT JSON AT ALL") + + # Also create eval-sets if testing evaluators (required sibling dir) + if "evaluations" in subdir: + (temp_project_dir / "evaluations" / "eval-sets").mkdir( + parents=True, exist_ok=True + ) + + result = load_agent_definition(temp_project_dir) + assert result.id == "test-malformed" diff --git a/tests/agent/utils/test_text_tokens.py b/tests/agent/utils/test_text_tokens.py index abdbeec00..b83beec42 100644 --- a/tests/agent/utils/test_text_tokens.py +++ b/tests/agent/utils/test_text_tokens.py @@ -277,3 +277,61 @@ def test_boolean(self): """Test serializing booleans (JSON-style lowercase).""" assert serialize_argument(True) == "true" assert serialize_argument(False) == "false" + + +class TestBuildStringFromTokensEdgeCases: + """Edge case tests for build_string_from_tokens.""" + + def test_non_ascii_in_tools_and_input(self): + """Non-ASCII characters in tool names and input values resolve correctly.""" + tokens = [ + TextToken(type=TextTokenType.SIMPLE_TEXT, raw_string="Use "), + TextToken(type=TextTokenType.VARIABLE, raw_string="tools.données"), + TextToken(type=TextTokenType.SIMPLE_TEXT, raw_string=" for "), + TextToken(type=TextTokenType.VARIABLE, raw_string="input.name"), + ] + result = build_string_from_tokens( + tokens, {"name": "日本語テスト"}, tool_names=["données"] + ) + assert result == "Use données for 日本語テスト" + + def test_unresolved_tool_reference_without_tool_names(self): + """Tool references return raw string when tool_names is None or empty.""" + tokens = [ + TextToken(type=TextTokenType.VARIABLE, raw_string="tools.search"), + ] + assert build_string_from_tokens(tokens, {}, tool_names=None) == "tools.search" + assert build_string_from_tokens(tokens, {}, tool_names=[]) == "tools.search" + + @pytest.mark.parametrize( + "input_data,raw_string,expected", + [ + ({"user": {"email": None}}, "input.user.email", "input.user.email"), + ({"name": ""}, "input.name", ""), + ({"count": 0}, "input.count", "0"), + ({"active": False}, "input.active", "false"), + ], + ids=["nested-none", "empty-string", "zero", "false"], + ) + def test_falsy_input_values(self, input_data, raw_string, expected): + """Falsy values (None, empty, 0, False) are handled distinctly.""" + tokens = [TextToken(type=TextTokenType.VARIABLE, raw_string=raw_string)] + result = build_string_from_tokens(tokens, input_data) + assert result == expected + + +class TestSafeGetNestedEdgeCases: + """Edge case tests for safe_get_nested.""" + + @pytest.mark.parametrize( + "data,path,expected", + [ + ({}, "any.path", None), + ({"a": "not_a_dict"}, "a.b", None), + ({"a": [1, 2, 3]}, "a.0", None), + ], + ids=["empty-dict", "non-dict-intermediate", "list-intermediate"], + ) + def test_unreachable_paths_return_none(self, data, path, expected): + """Paths through non-dict intermediates return None.""" + assert safe_get_nested(data, path) is expected