diff --git a/src/google/adk/tools/skill_toolset.py b/src/google/adk/tools/skill_toolset.py index 7d8e06368e..509f166e1f 100644 --- a/src/google/adk/tools/skill_toolset.py +++ b/src/google/adk/tools/skill_toolset.py @@ -644,7 +644,10 @@ def _build_wrapper_code( " full_path = os.path.join(td, rel_path)", " os.makedirs(os.path.dirname(full_path), exist_ok=True)", " mode = 'wb' if isinstance(content, bytes) else 'w'", - " with open(full_path, mode) as f:", + ( + " with open(full_path, mode, encoding='utf-8' if mode == 'w'" + " else None) as f:" + ), " f.write(content)", " os.chdir(td)", " try:", diff --git a/tests/unittests/tools/test_skill_toolset.py b/tests/unittests/tools/test_skill_toolset.py index f637377513..e6da74a2d0 100644 --- a/tests/unittests/tools/test_skill_toolset.py +++ b/tests/unittests/tools/test_skill_toolset.py @@ -670,6 +670,31 @@ async def test_execute_script_shell_success(mock_skill1): assert "__shell_result__" in code_input.code +@pytest.mark.asyncio +async def test_build_wrapper_code_with_unicode(mock_skill1): + """Verify that generated code uses utf-8 encoding for materializing files.""" + # Add unicode content to mock_skill1 resources + unicode_content = "你好" + mock_skill1.resources.list_references.return_value = ["unicode.txt"] + mock_skill1.resources.get_reference.side_effect = lambda name: ( + unicode_content if name == "unicode.txt" else None + ) + + executor = _make_mock_executor() + toolset = skill_toolset.SkillToolset([mock_skill1], code_executor=executor) + tool = skill_toolset.RunSkillScriptTool(toolset) + ctx = _make_tool_context_with_agent() + await tool.run_async( + args={"skill_name": "skill1", "file_path": "run.py"}, + tool_context=ctx, + ) + + call_args = executor.execute_code.call_args + code_input = call_args[0][1] + assert "encoding='utf-8' if mode == 'w' else None" in code_input.code + assert unicode_content in code_input.code + + @pytest.mark.asyncio async def test_execute_script_with_input_args_python(mock_skill1): executor = _make_mock_executor(stdout="done\n") @@ -1097,6 +1122,35 @@ async def test_integration_python_stdout(): assert result["stderr"] == "" +@pytest.mark.asyncio +async def test_integration_python_unicode_materialization(): + """Real executor: Python script with unicode resources.""" + script = models.Script( + src=( + "with open('references/unicode.txt', 'r', encoding='utf-8') as f:" + " print(f.read())" + ) + ) + skill = _make_skill_with_script("test_skill", "unicode.py", script) + skill.resources.get_reference.side_effect = lambda n: ( + "你好,世界" if n == "unicode.txt" else None + ) + skill.resources.list_references.return_value = ["unicode.txt"] + toolset = _make_real_executor_toolset([skill]) + tool = skill_toolset.RunSkillScriptTool(toolset) + ctx = _make_tool_context_with_agent() + result = await tool.run_async( + args={ + "skill_name": "test_skill", + "file_path": "unicode.py", + }, + tool_context=ctx, + ) + assert "status" in result, f"Result missing status: {result}" + assert result["status"] == "success" + assert "你好,世界" in result["stdout"] + + @pytest.mark.asyncio async def test_integration_python_sys_exit_zero(): """Real executor: sys.exit(0) is treated as success."""