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
253 lines
7.5 KiB
Python
253 lines
7.5 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 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"},
|
|
})
|