# 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. """Tests for CacheMetadata.""" import time from google.adk.models.cache_metadata import CacheMetadata from pydantic import ValidationError import pytest class TestCacheMetadata: """Test suite for CacheMetadata.""" def test_required_fields(self): """Test that all required fields must be provided.""" # Valid creation with all required fields metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=5, contents_count=3, ) assert ( metadata.cache_name == "projects/123/locations/us-central1/cachedContents/456" ) assert metadata.expire_time > time.time() assert metadata.fingerprint == "abc123" assert metadata.invocations_used == 5 assert metadata.contents_count == 3 assert metadata.created_at is None # Optional field def test_optional_created_at(self): """Test that created_at is optional.""" current_time = time.time() metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=3, contents_count=2, created_at=current_time, ) assert metadata.created_at == current_time def test_invocations_used_validation(self): """Test invocations_used validation constraints.""" # Valid: zero or positive metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=0, contents_count=1, ) assert metadata.invocations_used == 0 metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=10, contents_count=1, ) assert metadata.invocations_used == 10 # Invalid: negative with pytest.raises(ValidationError) as exc_info: CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=-1, contents_count=1, ) assert "greater than or equal to 0" in str(exc_info.value) def test_contents_count_validation(self): """Test contents_count validation constraints.""" # Valid: zero or positive metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=1, contents_count=0, ) assert metadata.contents_count == 0 metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=1, contents_count=10, ) assert metadata.contents_count == 10 # Invalid: negative with pytest.raises(ValidationError) as exc_info: CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=1, contents_count=-1, ) assert "greater than or equal to 0" in str(exc_info.value) def test_expire_soon_property(self): """Test expire_soon property.""" # Cache that expires in 10 minutes (should not expire soon) future_time = time.time() + 600 # 10 minutes metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=future_time, fingerprint="abc123", invocations_used=1, contents_count=1, ) assert not metadata.expire_soon # Cache that expires in 1 minute (should expire soon) soon_time = time.time() + 60 # 1 minute metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=soon_time, fingerprint="abc123", invocations_used=1, contents_count=1, ) assert metadata.expire_soon def test_str_representation(self): """Test string representation.""" current_time = time.time() expire_time = current_time + 1800 # 30 minutes metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/test456", expire_time=expire_time, fingerprint="abc123", invocations_used=7, contents_count=4, ) str_repr = str(metadata) assert "test456" in str_repr # Cache ID assert "used 7 invocations" in str_repr assert "cached 4 contents" in str_repr assert "expires in" in str_repr def test_immutability(self): """Test that CacheMetadata is immutable (frozen).""" metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=5, contents_count=3, ) # Should not be able to modify fields with pytest.raises(ValidationError): metadata.invocations_used = 10 def test_model_config(self): """Test that model config is set correctly.""" metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=5, contents_count=3, ) assert metadata.model_config["extra"] == "forbid" assert metadata.model_config["frozen"] == True def test_field_descriptions(self): """Test that fields have proper descriptions.""" metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=5, contents_count=3, ) schema = metadata.model_json_schema() assert "invocations_used" in schema["properties"] assert ( "Number of invocations" in schema["properties"]["invocations_used"]["description"] ) assert "contents_count" in schema["properties"] assert ( "Number of contents" in schema["properties"]["contents_count"]["description"] ) def test_realistic_cache_scenarios(self): """Test realistic cache scenarios.""" current_time = time.time() # Fresh cache fresh_cache = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/fresh123", expire_time=current_time + 1800, fingerprint="fresh_fingerprint", invocations_used=1, contents_count=5, created_at=current_time, ) assert fresh_cache.invocations_used == 1 assert not fresh_cache.expire_soon # Well-used cache used_cache = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/used456", expire_time=current_time + 600, fingerprint="used_fingerprint", invocations_used=8, contents_count=3, created_at=current_time - 1200, ) assert used_cache.invocations_used == 8 # Expiring cache expiring_cache = CacheMetadata( cache_name=( "projects/123/locations/us-central1/cachedContents/expiring789" ), expire_time=current_time + 60, # 1 minute fingerprint="expiring_fingerprint", invocations_used=15, contents_count=10, ) assert expiring_cache.expire_soon def test_cache_name_extraction(self): """Test cache name ID extraction in string representation.""" metadata = CacheMetadata( cache_name=( "projects/123/locations/us-central1/cachedContents/extracted_id" ), expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=1, contents_count=2, ) str_repr = str(metadata) assert "extracted_id" in str_repr def test_no_performance_metrics(self): """Test that performance metrics are not in CacheMetadata.""" metadata = CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/456", expire_time=time.time() + 1800, fingerprint="abc123", invocations_used=5, contents_count=3, ) # Verify that token counts are NOT in CacheMetadata # (they should be in LlmResponse.usage_metadata) assert not hasattr(metadata, "cached_tokens") assert not hasattr(metadata, "total_tokens") assert not hasattr(metadata, "prompt_tokens") def test_missing_required_fields(self): """Test validation when truly required fields are missing.""" # Only fingerprint and contents_count are required now # Other fields are optional (for fingerprint-only state) required_fields = [ "fingerprint", "contents_count", ] base_args = { "fingerprint": "abc123", "contents_count": 2, } for field in required_fields: args = base_args.copy() del args[field] with pytest.raises(ValidationError): CacheMetadata(**args) # Test that optional fields can be omitted (fingerprint-only state) metadata = CacheMetadata( fingerprint="abc123", contents_count=5, ) assert metadata.cache_name is None assert metadata.expire_time is None assert metadata.invocations_used is None assert metadata.created_at is None def test_partial_active_state_rejected(self): """cache_name, expire_time, invocations_used must all be set or all None.""" # Only cache_name set. with pytest.raises(ValidationError, match="must all be set"): CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/x", fingerprint="abc", contents_count=1, ) # cache_name + expire_time but no invocations_used. with pytest.raises(ValidationError, match="must all be set"): CacheMetadata( cache_name="projects/123/locations/us-central1/cachedContents/x", expire_time=time.time() + 1800, fingerprint="abc", contents_count=1, ) # invocations_used set without cache_name (e.g. construction bug). with pytest.raises(ValidationError, match="must all be set"): CacheMetadata( fingerprint="abc", invocations_used=3, contents_count=1, )