Files
google--adk-python/tests/unittests/skills/test__utils.py
T
wehub-resource-sync ec2b666284
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:25:13 +08:00

396 lines
12 KiB
Python

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unit tests for skill utilities."""
import builtins
import io
import sys
from unittest import mock
import zipfile
from google.adk.skills import list_skills_in_dir
from google.adk.skills import list_skills_in_gcs_dir as _list_skills_in_gcs_dir
from google.adk.skills import load_skill_from_dir as _load_skill_from_dir
from google.adk.skills import load_skill_from_gcs_dir as _load_skill_from_gcs_dir
from google.adk.skills._utils import _load_skill_from_zip_bytes
from google.adk.skills._utils import _read_skill_properties
from google.adk.skills._utils import _validate_skill_dir
import pytest
def test__load_skill_from_dir(tmp_path):
"""Tests loading a skill from a directory."""
skill_dir = tmp_path / "test-skill"
skill_dir.mkdir()
skill_md_content = """---
name: test-skill
description: Test description
---
Test instructions
"""
(skill_dir / "SKILL.md").write_text(skill_md_content)
# Create references
ref_dir = skill_dir / "references"
ref_dir.mkdir()
(ref_dir / "ref1.md").write_text("ref1 content")
# Create assets
assets_dir = skill_dir / "assets"
assets_dir.mkdir()
(assets_dir / "asset1.txt").write_text("asset1 content")
# Create scripts
scripts_dir = skill_dir / "scripts"
scripts_dir.mkdir()
(scripts_dir / "script1.sh").write_text("echo hello")
skill = _load_skill_from_dir(skill_dir)
assert skill.name == "test-skill"
assert skill.description == "Test description"
assert skill.instructions == "Test instructions"
assert skill.resources.get_reference("ref1.md") == "ref1 content"
assert skill.resources.get_asset("asset1.txt") == "asset1 content"
assert skill.resources.get_script("script1.sh").src == "echo hello"
def test_allowed_tools_yaml_key(tmp_path):
"""Tests that allowed-tools YAML key loads correctly."""
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
skill_md = """---
name: my-skill
description: A skill
allowed-tools: "some-tool-*"
---
Instructions here
"""
(skill_dir / "SKILL.md").write_text(skill_md)
skill = _load_skill_from_dir(skill_dir)
assert skill.frontmatter.allowed_tools == "some-tool-*"
def test_name_directory_mismatch(tmp_path):
"""Tests that name-directory mismatch raises ValueError."""
skill_dir = tmp_path / "wrong-dir"
skill_dir.mkdir()
skill_md = """---
name: my-skill
description: A skill
---
Body
"""
(skill_dir / "SKILL.md").write_text(skill_md)
with pytest.raises(ValueError, match="does not match directory"):
_load_skill_from_dir(skill_dir)
def test_validate_skill_dir_valid(tmp_path):
"""Tests validate_skill_dir with a valid skill."""
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
skill_md = """---
name: my-skill
description: A skill
---
Body
"""
(skill_dir / "SKILL.md").write_text(skill_md)
problems = _validate_skill_dir(skill_dir)
assert problems == []
def test_validate_skill_dir_missing_dir(tmp_path):
"""Tests validate_skill_dir with missing directory."""
problems = _validate_skill_dir(tmp_path / "nonexistent")
assert len(problems) == 1
assert "does not exist" in problems[0]
def test_validate_skill_dir_missing_skill_md(tmp_path):
"""Tests validate_skill_dir with missing SKILL.md."""
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
problems = _validate_skill_dir(skill_dir)
assert len(problems) == 1
assert "SKILL.md not found" in problems[0]
def test_validate_skill_dir_name_mismatch(tmp_path):
"""Tests validate_skill_dir catches name-directory mismatch."""
skill_dir = tmp_path / "wrong-dir"
skill_dir.mkdir()
skill_md = """---
name: my-skill
description: A skill
---
Body
"""
(skill_dir / "SKILL.md").write_text(skill_md)
problems = _validate_skill_dir(skill_dir)
assert any("does not match" in p for p in problems)
def test_validate_skill_dir_unknown_fields(tmp_path):
"""Tests validate_skill_dir detects unknown frontmatter fields."""
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
skill_md = """---
name: my-skill
description: A skill
unknown-field: something
---
Body
"""
(skill_dir / "SKILL.md").write_text(skill_md)
problems = _validate_skill_dir(skill_dir)
assert any("Unknown frontmatter" in p for p in problems)
def test__read_skill_properties(tmp_path):
"""Tests read_skill_properties basic usage."""
skill_dir = tmp_path / "my-skill"
skill_dir.mkdir()
skill_md = """---
name: my-skill
description: A cool skill
license: MIT
---
Body content
"""
(skill_dir / "SKILL.md").write_text(skill_md)
fm = _read_skill_properties(skill_dir)
assert fm.name == "my-skill"
assert fm.description == "A cool skill"
assert fm.license == "MIT"
@mock.patch("google.cloud.storage.Client")
def test__list_skills_in_gcs_dir(mock_client_class):
mock_client = mock.MagicMock()
mock_client_class.return_value = mock_client
mock_bucket = mock.MagicMock()
mock_client.bucket.return_value = mock_bucket
mock_iterator = mock.MagicMock()
mock_iterator.prefixes = ["skills/my-skill/"]
mock_bucket.list_blobs.return_value = mock_iterator
mock_blob = mock.MagicMock()
mock_blob.exists.return_value = True
mock_blob.download_as_text.return_value = (
"---\nname: my-skill\ndescription: A skill\n---\nBody"
)
mock_bucket.blob.return_value = mock_blob
skills = _list_skills_in_gcs_dir("my-bucket", "skills/")
assert "my-skill" in skills
assert skills["my-skill"].name == "my-skill"
@mock.patch("google.cloud.storage.Client")
@mock.patch("logging.warning")
def test__list_skills_in_gcs_dir_skips_invalid(
mock_logging_warning, mock_client_class
):
mock_client = mock.MagicMock()
mock_client_class.return_value = mock_client
mock_bucket = mock.MagicMock()
mock_client.bucket.return_value = mock_bucket
mock_iterator = mock.MagicMock()
mock_iterator.prefixes = ["skills/invalid-skill/", "skills/valid-skill/"]
mock_bucket.list_blobs.return_value = mock_iterator
def mock_blob_side_effect(path):
m = mock.MagicMock()
m.exists.return_value = True
if "invalid-skill" in path:
m.download_as_text.return_value = "invalid yaml content"
else:
m.download_as_text.return_value = (
"---\nname: valid-skill\ndescription: A skill\n---\nBody"
)
return m
mock_bucket.blob.side_effect = mock_blob_side_effect
skills = _list_skills_in_gcs_dir("my-bucket", "skills/")
assert "valid-skill" in skills
assert "invalid-skill" not in skills
# Verify warning was logged for the invalid skill
mock_logging_warning.assert_called_once()
args, _ = mock_logging_warning.call_args
assert "Skipping invalid skill" in args[0]
assert args[1] == "invalid-skill"
assert args[2] == "my-bucket"
@mock.patch("google.cloud.storage.Client")
def test__load_skill_from_gcs_dir(mock_client_class):
mock_client = mock.MagicMock()
mock_client_class.return_value = mock_client
mock_bucket = mock.MagicMock()
mock_client.bucket.return_value = mock_bucket
def mock_blob_side_effect(path):
m = mock.MagicMock()
if path.endswith("SKILL.md"):
m.exists.return_value = True
m.download_as_text.return_value = (
"---\nname: my-skill\ndescription: Test description\n---\nTest"
" instructions"
)
else:
m.exists.return_value = False
return m
mock_bucket.blob.side_effect = mock_blob_side_effect
# For resources
def list_blobs_side_effect(prefix=None):
if prefix.endswith("references/"):
m = mock.MagicMock()
m.name = prefix + "ref1.md"
m.download_as_text.return_value = "ref1 content"
return [m]
return []
mock_bucket.list_blobs.side_effect = list_blobs_side_effect
skill = _load_skill_from_gcs_dir("my-bucket", "skills/my-skill/")
assert skill.name == "my-skill"
assert skill.description == "Test description"
assert skill.instructions == "Test instructions"
# Using dict access for reference
assert skill.resources.get_reference("ref1.md") == "ref1 content"
def test_list_skills_in_dir(tmp_path):
"""Tests listing skills in a directory."""
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
# Valid skill 1
skill1_dir = skills_dir / "skill1"
skill1_dir.mkdir()
(skill1_dir / "SKILL.md").write_text(
"---\nname: skill1\ndescription: desc1\n---\nbody"
)
# Valid skill 2
skill2_dir = skills_dir / "skill2"
skill2_dir.mkdir()
(skill2_dir / "SKILL.md").write_text(
"---\nname: skill2\ndescription: desc2\n---\nbody"
)
# Invalid skill: missing SKILL.md
(skills_dir / "invalid-no-md").mkdir()
# Invalid skill: invalid YAML
invalid_yaml_dir = skills_dir / "invalid-yaml"
invalid_yaml_dir.mkdir()
(invalid_yaml_dir / "SKILL.md").write_text("---\ninvalid: yaml: :\n---\nbody")
# Invalid skill: name mismatch
mismatch_dir = skills_dir / "mismatch"
mismatch_dir.mkdir()
(mismatch_dir / "SKILL.md").write_text(
"---\nname: other-name\ndescription: desc\n---\nbody"
)
skills = list_skills_in_dir(skills_dir)
assert len(skills) == 2
assert "skill1" in skills
assert "skill2" in skills
assert skills["skill1"].name == "skill1"
assert skills["skill2"].name == "skill2"
def test_list_skills_in_dir_missing_base_path(tmp_path):
"""Tests list_skills_in_dir with missing base directory."""
skills = list_skills_in_dir(tmp_path / "nonexistent")
assert skills == {}
def test__load_skill_from_zip_bytes():
"""Tests loading a skill directly from in-memory zip file bytes."""
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, "w") as z:
z.writestr(
"SKILL.md",
"---\nname: my-skill\ndescription: A skill\n---\nBody instructions",
)
z.writestr("references/ref1.md", "ref1 content")
z.writestr("scripts/script1.sh", "echo hello")
skill = _load_skill_from_zip_bytes(zip_buffer.getvalue())
assert skill.frontmatter.name == "my-skill"
assert skill.frontmatter.description == "A skill"
assert skill.instructions == "Body instructions"
assert skill.resources.get_reference("ref1.md") == "ref1 content"
assert skill.resources.get_script("script1.sh").src == "echo hello"
def test__list_skills_in_gcs_dir_import_error():
"""Tests list_skills_in_gcs_dir raises ImportError when storage missing."""
real_import = builtins.__import__
def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
if name == "google.cloud" and "storage" in (fromlist or ()):
raise ImportError("No module named 'google.cloud.storage'")
return real_import(name, globals, locals, fromlist, level)
with mock.patch("builtins.__import__", mock_import):
with pytest.raises(ImportError, match="google-cloud-storage is required"):
_list_skills_in_gcs_dir("my-bucket", "skills/")
def test__load_skill_from_gcs_dir_import_error():
"""Tests load_skill_from_gcs_dir raises ImportError when storage missing."""
real_import = builtins.__import__
def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
if name == "google.cloud" and "storage" in (fromlist or ()):
raise ImportError("No module named 'google.cloud.storage'")
return real_import(name, globals, locals, fromlist, level)
with mock.patch("builtins.__import__", mock_import):
with pytest.raises(ImportError, match="google-cloud-storage is required"):
_load_skill_from_gcs_dir("my-bucket", "skills/my-skill/")