# 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 models.""" from google.adk.features import FeatureName from google.adk.features._feature_registry import temporary_feature_override from google.adk.skills import models from pydantic import ValidationError import pytest def test_frontmatter(): """Tests Frontmatter model.""" frontmatter = models.Frontmatter( name="test-skill", description="Test description", license="Apache 2.0", compatibility="test", allowed_tools="test", metadata={"key": "value"}, ) assert frontmatter.name == "test-skill" assert frontmatter.description == "Test description" assert frontmatter.license == "Apache 2.0" assert frontmatter.compatibility == "test" assert frontmatter.allowed_tools == "test" assert frontmatter.metadata == {"key": "value"} def test_resources(): """Tests Resources model.""" resources = models.Resources( references={"ref1": "ref content"}, assets={"asset1": "asset content"}, scripts={"script1": models.Script(src="print('hello')")}, ) assert resources.get_reference("ref1") == "ref content" assert resources.get_asset("asset1") == "asset content" assert resources.get_script("script1").src == "print('hello')" assert resources.get_reference("ref2") is None assert resources.get_asset("asset2") is None assert resources.get_script("script2") is None assert resources.list_references() == ["ref1"] assert resources.list_assets() == ["asset1"] assert resources.list_scripts() == ["script1"] def test_skill_properties(): """Tests Skill model.""" frontmatter = models.Frontmatter( name="my-skill", description="my description" ) skill = models.Skill(frontmatter=frontmatter, instructions="do this") assert skill.name == "my-skill" assert skill.description == "my description" def test_script_to_string(): """Tests Script model.""" script = models.Script(src="print('hello')") assert str(script) == "print('hello')" # --- Name validation tests --- def test_name_too_long(): with pytest.raises(ValidationError, match="at most 64 characters"): models.Frontmatter(name="a" * 65, description="desc") def test_name_uppercase_rejected(): with pytest.raises(ValidationError, match="lowercase kebab-case"): models.Frontmatter(name="My-Skill", description="desc") def test_name_leading_hyphen(): with pytest.raises(ValidationError, match="lowercase kebab-case"): models.Frontmatter(name="-my-skill", description="desc") def test_name_trailing_hyphen(): with pytest.raises(ValidationError, match="lowercase kebab-case"): models.Frontmatter(name="my-skill-", description="desc") def test_name_consecutive_hyphens(): with pytest.raises(ValidationError, match="lowercase kebab-case"): models.Frontmatter(name="my--skill", description="desc") def test_name_underscore_rejected_by_default(): with pytest.raises(ValidationError, match="lowercase kebab-case"): models.Frontmatter(name="my_skill", description="desc") def test_name_valid_underscore_preserved_with_flag(): with temporary_feature_override(FeatureName.SNAKE_CASE_SKILL_NAME, True): fm = models.Frontmatter(name="my_skill", description="desc") assert fm.name == "my_skill" def test_name_invalid_chars_ampersand(): with pytest.raises( ValidationError, match="name must be lowercase kebab-case" ): models.Frontmatter(name="skill&name", description="desc") def test_name_mixed_delimiters_rejected_by_default(): with pytest.raises( ValidationError, match="name must be lowercase kebab-case" ): models.Frontmatter(name="my-skill_1", description="desc") def test_name_mixed_delimiters_rejected_with_flag(): with temporary_feature_override(FeatureName.SNAKE_CASE_SKILL_NAME, True): with pytest.raises( ValidationError, match="Mixing hyphens and underscores is not allowed" ): models.Frontmatter(name="my-skill_1", description="desc") def test_name_valid_passes(): fm = models.Frontmatter(name="my-skill-2", description="desc") assert fm.name == "my-skill-2" def test_name_single_word(): fm = models.Frontmatter(name="skill", description="desc") assert fm.name == "skill" # --- Description validation tests --- def test_description_empty(): with pytest.raises(ValidationError, match="must not be empty"): models.Frontmatter(name="my-skill", description="") def test_description_too_long(): with pytest.raises( ValidationError, match="at most 1024 characters. Description length: 1025", ): models.Frontmatter(name="my-skill", description="x" * 1025) # --- Compatibility validation tests --- def test_compatibility_too_long(): with pytest.raises(ValidationError, match="at most 500 characters"): models.Frontmatter( name="my-skill", description="desc", compatibility="c" * 501 ) # --- Extra field rejected --- def test_extra_field_allowed(): fm = models.Frontmatter.model_validate({ "name": "my-skill", "description": "desc", "unknown_field": "value", }) assert fm.name == "my-skill" # --- allowed-tools alias --- def test_allowed_tools_alias_via_model_validate(): fm = models.Frontmatter.model_validate({ "name": "my-skill", "description": "desc", "allowed-tools": "tool-pattern", }) assert fm.allowed_tools == "tool-pattern" def test_allowed_tools_serialization_alias(): fm = models.Frontmatter( name="my-skill", description="desc", allowed_tools="tool-pattern" ) dumped = fm.model_dump(by_alias=True) assert "allowed-tools" in dumped assert dumped["allowed-tools"] == "tool-pattern" def test_metadata_adk_additional_tools_list(): fm = models.Frontmatter.model_validate({ "name": "my-skill", "description": "desc", "metadata": {"adk_additional_tools": ["tool1", "tool2"]}, }) assert fm.metadata["adk_additional_tools"] == ["tool1", "tool2"] def test_metadata_adk_additional_tools_rejected_as_string(): with pytest.raises( ValidationError, match="adk_additional_tools must be a list of strings" ): models.Frontmatter.model_validate({ "name": "my-skill", "description": "desc", "metadata": {"adk_additional_tools": "tool1 tool2"}, }) def test_metadata_adk_additional_tools_invalid_type(): with pytest.raises( ValidationError, match="adk_additional_tools must be a list of strings" ): models.Frontmatter.model_validate({ "name": "my-skill", "description": "desc", "metadata": {"adk_additional_tools": 123}, }) def test_metadata_adk_inject_state_bool(): fm = models.Frontmatter.model_validate({ "name": "my-skill", "description": "desc", "metadata": {"adk_inject_state": True}, }) assert fm.metadata["adk_inject_state"] is True def test_metadata_adk_inject_state_rejected_as_string(): with pytest.raises(ValidationError, match="adk_inject_state must be a bool"): models.Frontmatter.model_validate({ "name": "my-skill", "description": "desc", "metadata": {"adk_inject_state": "true"}, })