From bc4be37f5f867392f8d030435caf8c2679880150 Mon Sep 17 00:00:00 2001 From: A Vertex SDK engineer Date: Mon, 18 May 2026 13:07:26 -0700 Subject: [PATCH] feat: Make skill_id a required parameter in `skills.create` The `skill_id` parameter is now a required keyword argument in the `skills.create()` method. It was previously an optional field within the `config` object. PiperOrigin-RevId: 917387423 --- agentplatform/_genai/skills.py | 20 +- agentplatform/_genai/types/common.py | 22 +- .../genai/replays/test_skills_create.py | 72 +- .../genai/replays/test_skills_delete.py | 41 +- .../replays/test_skills_revisions_get.py | 59 +- .../replays/test_skills_revisions_list.py | 51 +- .../genai/replays/test_skills_update.py | 82 +- .../agentplatform/genai/test_genai_skills.py | 1915 +++++++++-------- 8 files changed, 1156 insertions(+), 1106 deletions(-) diff --git a/agentplatform/_genai/skills.py b/agentplatform/_genai/skills.py index d340a67e50..d2b670b22b 100644 --- a/agentplatform/_genai/skills.py +++ b/agentplatform/_genai/skills.py @@ -55,9 +55,6 @@ def _CreateSkillConfig_to_vertex( getv(from_object, ["zipped_filesystem"]), ) - if getv(from_object, ["skill_id"]) is not None: - setv(parent_object, ["_query", "skillId"], getv(from_object, ["skill_id"])) - return to_object @@ -75,6 +72,9 @@ def _CreateSkillRequestParameters_to_vertex( if getv(from_object, ["config"]) is not None: _CreateSkillConfig_to_vertex(getv(from_object, ["config"]), to_object) + if getv(from_object, ["skill_id"]) is not None: + setv(to_object, ["_query", "skillId"], getv(from_object, ["skill_id"])) + return to_object @@ -367,6 +367,7 @@ def _create( display_name: str, description: str, config: Optional[types.CreateSkillConfigOrDict] = None, + skill_id: str, ) -> types.SkillOperation: """ Creates a new Skill. @@ -376,6 +377,7 @@ def _create( display_name=display_name, description=description, config=config, + skill_id=skill_id, ) request_url_dict: Optional[dict[str, str]] @@ -712,6 +714,7 @@ def _get_skill_operation( def create( self, *, + skill_id: str, display_name: str, description: str, config: Optional[types.CreateSkillConfigOrDict] = None, @@ -719,6 +722,9 @@ def create( """Creates a new Skill. Args: + skill_id (str): + Required. The ID to use for the Skill, which will become the final + component of the Skill's resource name. display_name (str): Required. The display name of the Skill. description (str): @@ -775,6 +781,7 @@ def create( config.zipped_filesystem = zipped_filesystem_payload operation = self._create( + skill_id=skill_id, display_name=display_name, description=description, config=config, @@ -1110,6 +1117,7 @@ async def _create( display_name: str, description: str, config: Optional[types.CreateSkillConfigOrDict] = None, + skill_id: str, ) -> types.SkillOperation: """ Creates a new Skill. @@ -1119,6 +1127,7 @@ async def _create( display_name=display_name, description=description, config=config, + skill_id=skill_id, ) request_url_dict: Optional[dict[str, str]] @@ -1465,6 +1474,7 @@ async def _get_skill_operation( async def create( self, *, + skill_id: str, display_name: str, description: str, config: Optional[types.CreateSkillConfigOrDict] = None, @@ -1472,6 +1482,9 @@ async def create( """Creates a new Skill asynchronously. Args: + skill_id (str): + Required. The ID to use for the Skill, which will become the final + component of the Skill's resource name. display_name (str): Required. The display name of the Skill. description (str): @@ -1529,6 +1542,7 @@ async def create( config.zipped_filesystem = zipped_filesystem_payload operation = await self._create( + skill_id=skill_id, display_name=display_name, description=description, config=config, diff --git a/agentplatform/_genai/types/common.py b/agentplatform/_genai/types/common.py index 392c58be2f..8effba5ac0 100644 --- a/agentplatform/_genai/types/common.py +++ b/agentplatform/_genai/types/common.py @@ -18262,12 +18262,6 @@ class CreateSkillConfig(_common.BaseModel): zipped_filesystem: Optional[Any] = Field( default=None, description="""Optional. The zipped filesystem of the Skill.""" ) - skill_id: Optional[str] = Field( - default=None, - description="""Optional. The ID to use for the Skill, which will become the final - component of the Skill's resource name. - """, - ) class CreateSkillConfigDict(TypedDict, total=False): @@ -18287,11 +18281,6 @@ class CreateSkillConfigDict(TypedDict, total=False): zipped_filesystem: Optional[Any] """Optional. The zipped filesystem of the Skill.""" - skill_id: Optional[str] - """Optional. The ID to use for the Skill, which will become the final - component of the Skill's resource name. - """ - CreateSkillConfigOrDict = Union[CreateSkillConfig, CreateSkillConfigDict] @@ -18306,6 +18295,12 @@ class _CreateSkillRequestParameters(_common.BaseModel): default=None, description="""Required. The description of the Skill.""" ) config: Optional[CreateSkillConfig] = Field(default=None, description="""""") + skill_id: Optional[str] = Field( + default=None, + description="""Required. The ID to use for the Skill, which will become the final + component of the Skill's resource name. + """, + ) class _CreateSkillRequestParametersDict(TypedDict, total=False): @@ -18320,6 +18315,11 @@ class _CreateSkillRequestParametersDict(TypedDict, total=False): config: Optional[CreateSkillConfigDict] """""" + skill_id: Optional[str] + """Required. The ID to use for the Skill, which will become the final + component of the Skill's resource name. + """ + _CreateSkillRequestParametersOrDict = Union[ _CreateSkillRequestParameters, _CreateSkillRequestParametersDict diff --git a/tests/unit/agentplatform/genai/replays/test_skills_create.py b/tests/unit/agentplatform/genai/replays/test_skills_create.py index f23c9a7479..4b23743ad1 100644 --- a/tests/unit/agentplatform/genai/replays/test_skills_create.py +++ b/tests/unit/agentplatform/genai/replays/test_skills_create.py @@ -28,47 +28,49 @@ def test_create_skill(client, tmp_path): - client._api_client._http_options.base_url = ( - "https://us-central1-aiplatform.googleapis.com" - ) + client._api_client._http_options.base_url = ( + "https://us-central1-aiplatform.googleapis.com" + ) - # Create a dummy skill structure (SKILL.md is required by the spec) - with open(tmp_path / "SKILL.md", "w") as f: - f.write("# My Replay Skill\nThis is a test skill for replay tests.") + # Create a dummy skill structure (SKILL.md is required by the spec) + with open(tmp_path / "SKILL.md", "w") as f: + f.write("# My Replay Skill\nThis is a test skill for replay tests.") - skill = client.skills.create( - display_name="My Replay Skill", - description="My Replay Skill Description", - config=types.CreateSkillConfig( - local_path=str(tmp_path), wait_for_completion=True - ), - ) + skill = client.skills.create( + skill_id="my-replay-skill-v2", + display_name="My Replay Skill", + description="My Replay Skill Description", + config=types.CreateSkillConfig( + local_path=str(tmp_path), wait_for_completion=True + ), + ) - assert skill.name is not None - assert skill.display_name == "My Replay Skill" - assert skill.description == "My Replay Skill Description" + assert skill.name is not None + assert skill.display_name == "My Replay Skill" + assert skill.description == "My Replay Skill Description" def test_create_skill_with_prezipped_bytes(client): - """Tests the creation of a skill with pre-zipped bytes.""" - client._api_client._http_options.base_url = ( - "https://us-central1-aiplatform.googleapis.com" - ) + """Tests the creation of a skill with pre-zipped bytes.""" + client._api_client._http_options.base_url = ( + "https://us-central1-aiplatform.googleapis.com" + ) - zip_buffer = io.BytesIO() - zinfo = zipfile.ZipInfo("SKILL.md", date_time=(1980, 1, 1, 0, 0, 0)) - with zipfile.ZipFile(zip_buffer, "w") as zip_file: - zip_file.writestr(zinfo, "# My Zipped Replay Skill\nThis is a test.") - zipped_bytes = zip_buffer.getvalue() + zip_buffer = io.BytesIO() + zinfo = zipfile.ZipInfo("SKILL.md", date_time=(1980, 1, 1, 0, 0, 0)) + with zipfile.ZipFile(zip_buffer, "w") as zip_file: + zip_file.writestr(zinfo, "# My Zipped Replay Skill\nThis is a test.") + zipped_bytes = zip_buffer.getvalue() - skill = client.skills.create( - display_name="My Zipped Replay Skill", - description="My Zipped Replay Skill Description", - config=types.CreateSkillConfig( - zipped_filesystem=zipped_bytes, wait_for_completion=True - ), - ) + skill = client.skills.create( + skill_id="my-zipped-replay-skill-v2", + display_name="My Zipped Replay Skill", + description="My Zipped Replay Skill Description", + config=types.CreateSkillConfig( + zipped_filesystem=zipped_bytes, wait_for_completion=True + ), + ) - assert skill.name is not None - assert skill.display_name == "My Zipped Replay Skill" - assert skill.description == "My Zipped Replay Skill Description" + assert skill.name is not None + assert skill.display_name == "My Zipped Replay Skill" + assert skill.description == "My Zipped Replay Skill Description" diff --git a/tests/unit/agentplatform/genai/replays/test_skills_delete.py b/tests/unit/agentplatform/genai/replays/test_skills_delete.py index f407f6590a..a8bd0ba2f2 100644 --- a/tests/unit/agentplatform/genai/replays/test_skills_delete.py +++ b/tests/unit/agentplatform/genai/replays/test_skills_delete.py @@ -13,28 +13,29 @@ def test_delete_skill(client, tmp_path): - # 1. Create a fresh unique skill first - with open(tmp_path / "SKILL.md", "w") as f: - f.write("# Test Skill\nTo be deleted.") - - created_skill = client.skills.create( - display_name="To Be Deleted Skill", - description="Skill to be deleted", - config=types.CreateSkillConfig( - local_path=str(tmp_path), wait_for_completion=True - ), - ) - - assert created_skill.name is not None - - # 2. Delete the skill and wait for LRO completion - client.skills.delete( + # 1. Create a fresh unique skill first + with open(tmp_path / "SKILL.md", "w") as f: + f.write("# Test Skill\nTo be deleted.") + + created_skill = client.skills.create( + skill_id="my-skill-to-delete", + display_name="To Be Deleted Skill", + description="Skill to be deleted", + config=types.CreateSkillConfig( + local_path=str(tmp_path), wait_for_completion=True + ), + ) + + assert created_skill.name is not None + + # 2. Delete the skill and wait for LRO completion + client.skills.delete( name=created_skill.name, config=types.DeleteSkillConfig(wait_for_completion=True), ) - # 3. Verify the skill is successfully deleted (Getting it should raise NotFound) - with pytest.raises(errors.ClientError) as exc_info: - client.skills.get(name=created_skill.name) + # 3. Verify the skill is successfully deleted (Getting it should raise NotFound) + with pytest.raises(errors.ClientError) as exc_info: + client.skills.get(name=created_skill.name) - assert exc_info.value.code == 404 + assert exc_info.value.code == 404 diff --git a/tests/unit/agentplatform/genai/replays/test_skills_revisions_get.py b/tests/unit/agentplatform/genai/replays/test_skills_revisions_get.py index 52808d6c63..d77723f8a1 100644 --- a/tests/unit/agentplatform/genai/replays/test_skills_revisions_get.py +++ b/tests/unit/agentplatform/genai/replays/test_skills_revisions_get.py @@ -19,45 +19,46 @@ def test_get_skill_revision(client, tmp_path): - # Target the autopush sandbox endpoint for the Skill Registry API - client._api_client._http_options.base_url = ( + # Target the autopush sandbox endpoint for the Skill Registry API + client._api_client._http_options.base_url = ( "https://us-central1-autopush-aiplatform.sandbox.googleapis.com" ) - # 1. Create a fresh unique skill - with open(tmp_path / "SKILL.md", "w") as f: - f.write("# Replay Revision Test Skill\nThis is a test skill.") + # 1. Create a fresh unique skill + with open(tmp_path / "SKILL.md", "w") as f: + f.write("# Replay Revision Test Skill\nThis is a test skill.") - created_skill = client.skills.create( - display_name="Replay Revision Test Skill", - description="A temporary skill to test revisions E2E", - config=types.CreateSkillConfig( - local_path=str(tmp_path), wait_for_completion=True - ), - ) + created_skill = client.skills.create( + skill_id="my-skill-to-get-revision", + display_name="Replay Revision Test Skill", + description="A temporary skill to test revisions E2E", + config=types.CreateSkillConfig( + local_path=str(tmp_path), wait_for_completion=True + ), + ) - try: - assert created_skill.name is not None + try: + assert created_skill.name is not None - # 2. List revisions to dynamically discover the revision ID - revisions_response = client.skills.revisions.list(name=created_skill.name) - revisions_list = revisions_response.skill_revisions + # 2. List revisions to dynamically discover the revision ID + revisions_response = client.skills.revisions.list(name=created_skill.name) + revisions_list = revisions_response.skill_revisions - assert len(revisions_list) > 0 - first_revision = revisions_list[0] - assert isinstance(first_revision, types.SkillRevision) - assert first_revision.name is not None + assert len(revisions_list) > 0 + first_revision = revisions_list[0] + assert isinstance(first_revision, types.SkillRevision) + assert first_revision.name is not None - # 3. Explicitly GET the revision by its resource name - revision = client.skills.revisions.get(name=first_revision.name) + # 3. Explicitly GET the revision by its resource name + revision = client.skills.revisions.get(name=first_revision.name) - assert isinstance(revision, types.SkillRevision) - assert revision.name == first_revision.name - assert revision.state == types.SkillState.ACTIVE + assert isinstance(revision, types.SkillRevision) + assert revision.name == first_revision.name + assert revision.state == types.SkillState.ACTIVE - finally: - # 4. Clean up the temporary skill - client.skills.delete( + finally: + # 4. Clean up the temporary skill + client.skills.delete( name=created_skill.name, config=types.DeleteSkillConfig(wait_for_completion=True), ) diff --git a/tests/unit/agentplatform/genai/replays/test_skills_revisions_list.py b/tests/unit/agentplatform/genai/replays/test_skills_revisions_list.py index 99cf9bd19f..82ec7bbf92 100644 --- a/tests/unit/agentplatform/genai/replays/test_skills_revisions_list.py +++ b/tests/unit/agentplatform/genai/replays/test_skills_revisions_list.py @@ -19,39 +19,40 @@ def test_list_skill_revisions(client, tmp_path): - # Target the autopush sandbox endpoint for the Skill Registry API - client._api_client._http_options.base_url = ( + # Target the autopush sandbox endpoint for the Skill Registry API + client._api_client._http_options.base_url = ( "https://us-central1-autopush-aiplatform.sandbox.googleapis.com" ) - # 1. Create a fresh unique skill - with open(tmp_path / "SKILL.md", "w") as f: - f.write("# Replay List Revisions Test Skill\nThis is a test skill.") + # 1. Create a fresh unique skill + with open(tmp_path / "SKILL.md", "w") as f: + f.write("# Replay List Revisions Test Skill\nThis is a test skill.") - created_skill = client.skills.create( - display_name="Replay List Revisions Test Skill", - description="A temporary skill to test list revisions E2E", - config=types.CreateSkillConfig( - local_path=str(tmp_path), wait_for_completion=True - ), - ) + created_skill = client.skills.create( + skill_id="my-skill-to-list-revisions", + display_name="Replay List Revisions Test Skill", + description="A temporary skill to test list revisions E2E", + config=types.CreateSkillConfig( + local_path=str(tmp_path), wait_for_completion=True + ), + ) - try: - assert created_skill.name is not None + try: + assert created_skill.name is not None - # 2. List revisions - revisions_response = client.skills.revisions.list(name=created_skill.name) - revisions_list = revisions_response.skill_revisions + # 2. List revisions + revisions_response = client.skills.revisions.list(name=created_skill.name) + revisions_list = revisions_response.skill_revisions - assert len(revisions_list) > 0 - first_revision = revisions_list[0] - assert isinstance(first_revision, types.SkillRevision) - assert first_revision.name is not None - assert first_revision.state == types.SkillState.ACTIVE + assert len(revisions_list) > 0 + first_revision = revisions_list[0] + assert isinstance(first_revision, types.SkillRevision) + assert first_revision.name is not None + assert first_revision.state == types.SkillState.ACTIVE - finally: - # 3. Clean up the temporary skill - client.skills.delete( + finally: + # 3. Clean up the temporary skill + client.skills.delete( name=created_skill.name, config=types.DeleteSkillConfig(wait_for_completion=True), ) diff --git a/tests/unit/agentplatform/genai/replays/test_skills_update.py b/tests/unit/agentplatform/genai/replays/test_skills_update.py index 86ac5e11f8..d7e01c8308 100644 --- a/tests/unit/agentplatform/genai/replays/test_skills_update.py +++ b/tests/unit/agentplatform/genai/replays/test_skills_update.py @@ -29,20 +29,21 @@ def test_update_skill(client, tmp_path): - # 1. Create a fresh unique skill first - with open(tmp_path / "SKILL.md", "w") as f: - f.write("# Test Skill\nInitial content.") - - created_skill = client.skills.create( - display_name="Original Skill", - description="Original Description", - config=types.CreateSkillConfig( - local_path=str(tmp_path), wait_for_completion=True - ), - ) - - # 2. Perform the metadata-only update on the new skill - updated_skill = client.skills.update( + # 1. Create a fresh unique skill first + with open(tmp_path / "SKILL.md", "w") as f: + f.write("# Test Skill\nInitial content.") + + created_skill = client.skills.create( + skill_id="my-skill-to-update-1", + display_name="Original Skill", + description="Original Description", + config=types.CreateSkillConfig( + local_path=str(tmp_path), wait_for_completion=True + ), + ) + + # 2. Perform the metadata-only update on the new skill + updated_skill = client.skills.update( name=created_skill.name, config=types.UpdateSkillConfig( display_name="My Updated Replay Skill", @@ -51,41 +52,42 @@ def test_update_skill(client, tmp_path): ), ) - assert updated_skill.name == created_skill.name - assert updated_skill.display_name == "My Updated Replay Skill" - assert updated_skill.description == "My Updated Replay Skill Description" + assert updated_skill.name == created_skill.name + assert updated_skill.display_name == "My Updated Replay Skill" + assert updated_skill.description == "My Updated Replay Skill Description" def test_update_skill_with_zipped_bytes(client, tmp_path): - # 1. Create a fresh unique skill first - with open(tmp_path / "SKILL.md", "w") as f: - f.write("# Test Skill\nInitial content.") - - created_skill = client.skills.create( - display_name="Original Skill", - description="Original Description", - config=types.CreateSkillConfig( - local_path=str(tmp_path), wait_for_completion=True - ), - ) - - # 2. Prepare zipped bytes for update - zip_buffer = io.BytesIO() - zinfo = zipfile.ZipInfo("SKILL.md", date_time=(1980, 1, 1, 0, 0, 0)) - with zipfile.ZipFile(zip_buffer, "w") as zip_file: - zip_file.writestr(zinfo, "# My Updated Zipped Replay Skill\nThis is updated.") - zipped_bytes = zip_buffer.getvalue() - - # 3. Update the skill with new zipped bytes - updated_skill = client.skills.update( + # 1. Create a fresh unique skill first + with open(tmp_path / "SKILL.md", "w") as f: + f.write("# Test Skill\nInitial content.") + + created_skill = client.skills.create( + skill_id="my-skill-to-update-2", + display_name="Original Skill", + description="Original Description", + config=types.CreateSkillConfig( + local_path=str(tmp_path), wait_for_completion=True + ), + ) + + # 2. Prepare zipped bytes for update + zip_buffer = io.BytesIO() + zinfo = zipfile.ZipInfo("SKILL.md", date_time=(1980, 1, 1, 0, 0, 0)) + with zipfile.ZipFile(zip_buffer, "w") as zip_file: + zip_file.writestr(zinfo, "# My Updated Zipped Replay Skill\nThis is updated.") + zipped_bytes = zip_buffer.getvalue() + + # 3. Update the skill with new zipped bytes + updated_skill = client.skills.update( name=created_skill.name, config=types.UpdateSkillConfig( zipped_filesystem=zipped_bytes, wait_for_completion=True ), ) - assert updated_skill.name == created_skill.name - assert ( + assert updated_skill.name == created_skill.name + assert ( updated_skill.display_name == "Original Skill" ) # Display name remains unchanged diff --git a/tests/unit/agentplatform/genai/test_genai_skills.py b/tests/unit/agentplatform/genai/test_genai_skills.py index ee3fb71c93..36c73015f4 100644 --- a/tests/unit/agentplatform/genai/test_genai_skills.py +++ b/tests/unit/agentplatform/genai/test_genai_skills.py @@ -12,1004 +12,1033 @@ # See the License for the specific language governing permissions and # limitations under the License. # +import asyncio import json import os import tempfile +import time from unittest import mock import google.auth.credentials from agentplatform import _genai as genai from agentplatform._genai import client as agentplatform_client from google.genai import types as genai_types import pytest -import asyncio -import time @pytest.fixture def skills_client(): - creds = mock.create_autospec(google.auth.credentials.Credentials, instance=True) - creds.token = "test_token" - client = agentplatform_client.Client( - project="test-project", location="test-location", credentials=creds - ) - return client.skills + creds = mock.create_autospec( + google.auth.credentials.Credentials, instance=True + ) + creds.token = "test_token" + client = agentplatform_client.Client( + project="test-project", location="test-location", credentials=creds + ) + return client.skills @pytest.fixture def async_skills_client(): - creds = mock.create_autospec(google.auth.credentials.Credentials, instance=True) - creds.token = "test_token" - client = agentplatform_client.Client( - project="test-project", location="test-location", credentials=creds - ) - return client.aio.skills + creds = mock.create_autospec( + google.auth.credentials.Credentials, instance=True + ) + creds.token = "test_token" + client = agentplatform_client.Client( + project="test-project", location="test-location", credentials=creds + ) + return client.aio.skills class TestGenaiSkills: - mock_get_skill_response = { - "name": "projects/test-project/locations/test-location/skills/test-skill", - "displayName": "My Test Skill", + mock_get_skill_response = { + "name": "projects/test-project/locations/test-location/skills/test-skill", + "displayName": "My Test Skill", + } + + def test_get_skill(self, skills_client): + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(self.mock_get_skill_response) + ) + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + skill = skills_client.get(name=skill_name) + request_mock.assert_called_once_with( + "get", + skill_name, + {"_url": {"name": skill_name}}, + None, + ) + assert isinstance(skill, genai.types.Skill) + assert skill.name == skill_name + assert skill.display_name == "My Test Skill" + + def test_retrieve_skills_response(self, skills_client): + mock_retrieve_response = { + "retrievedSkills": [ + { + "skillName": ( + "projects/test-project/locations/test-location/skills/skill-1" + ), + "description": "Skill 1 Description", + }, + { + "skillName": ( + "projects/test-project/locations/test-location/skills/skill-2" + ), + "description": "Skill 2 Description", + }, + ] } - def test_get_skill(self, skills_client): - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(self.mock_get_skill_response) - ) - skill_name = ( - "projects/test-project/locations/test-location/skills/test-skill" - ) - skill = skills_client.get(name=skill_name) - request_mock.assert_called_once_with( - "get", - skill_name, - {"_url": {"name": skill_name}}, - None, - ) - assert isinstance(skill, genai.types.Skill) - assert skill.name == skill_name - assert skill.display_name == "My Test Skill" - - def test_retrieve_skills_response(self, skills_client): - mock_retrieve_response = { - "retrievedSkills": [ - { - "skillName": ( - "projects/test-project/locations/test-location/skills/skill-1" - ), - "description": "Skill 1 Description", - }, - { - "skillName": ( - "projects/test-project/locations/test-location/skills/skill-2" - ), - "description": "Skill 2 Description", - }, - ] - } - - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(mock_retrieve_response) - ) - - response = skills_client.retrieve(query="test query", config={"top_k": 5}) - - assert isinstance(response, genai.types.RetrieveSkillsResponse) - assert len(response.retrieved_skills) == 2 - assert response.retrieved_skills[0].skill_name == ( + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(mock_retrieve_response) + ) + + response = skills_client.retrieve(query="test query", config={"top_k": 5}) + + assert isinstance(response, genai.types.RetrieveSkillsResponse) + assert len(response.retrieved_skills) == 2 + assert response.retrieved_skills[0].skill_name == ( + "projects/test-project/locations/test-location/skills/skill-1" + ) + assert response.retrieved_skills[0].description == "Skill 1 Description" + + def test_retrieve_skills_request_params(self, skills_client): + mock_retrieve_response = {"retrievedSkills": []} + + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(mock_retrieve_response) + ) + + skills_client.retrieve(query="test query", config={"top_k": 5}) + + request_mock.assert_called_once_with( + "get", + "skills:retrieve?query=test+query&topK=5", + {"_query": {"query": "test query", "topK": 5}}, + None, + ) + + @pytest.mark.asyncio + async def test_retrieve_skills_async(self, async_skills_client): + mock_retrieve_response = { + "retrievedSkills": [{ + "skillName": ( "projects/test-project/locations/test-location/skills/skill-1" - ) - assert response.retrieved_skills[0].description == "Skill 1 Description" - - def test_retrieve_skills_request_params(self, skills_client): - mock_retrieve_response = {"retrievedSkills": []} - - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(mock_retrieve_response) - ) - - skills_client.retrieve(query="test query", config={"top_k": 5}) - - request_mock.assert_called_once_with( - "get", - "skills:retrieve?query=test+query&topK=5", - {"_query": {"query": "test query", "topK": 5}}, - None, - ) + ), + "description": "Skill 1 Description", + }] + } - @pytest.mark.asyncio - async def test_retrieve_skills_async(self, async_skills_client): - mock_retrieve_response = { - "retrievedSkills": [ + with mock.patch.object( + async_skills_client._api_client, "async_request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(mock_retrieve_response) + ) + + response = await async_skills_client.retrieve( + query="test query", config={"top_k": 1} + ) + + assert isinstance(response, genai.types.RetrieveSkillsResponse) + assert len(response.retrieved_skills) == 1 + assert response.retrieved_skills[0].skill_name == ( + "projects/test-project/locations/test-location/skills/skill-1" + ) + + request_mock.assert_called_once_with( + "get", + "skills:retrieve?query=test+query&topK=1", + {"_query": {"query": "test query", "topK": 1}}, + None, + ) + + def test_create_skill(self, skills_client): + """Tests the create_skill method with wait_for_completion=True.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Create a dummy file in tmpdir + with open(os.path.join(tmpdir, "SKILL.md"), "w") as f: + f.write("# Test Skill") + + # Prepare mock responses + pending_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" + ), + "done": False, + } + finished_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" + ), + "done": True, + "response": { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill" + ), + "displayName": "My Test Skill", + "description": "My Test Skill Description", + }, + } + + # Final Skill response returned by get call + skill_response = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill" + ), + "displayName": "My Test Skill", + "description": "My Test Skill Description", + } + + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.side_effect = [ + genai_types.HttpResponse(body=json.dumps(pending_op)), + genai_types.HttpResponse(body=json.dumps(finished_op)), + genai_types.HttpResponse(body=json.dumps(skill_response)), + ] + + # We mock time.sleep to speed up the test + with mock.patch.object(time, "sleep", return_value=None): + skill = skills_client.create( + skill_id="test-skill", + display_name="My Test Skill", + description="My Test Skill Description", + config={"local_path": tmpdir, "wait_for_completion": True}, + ) + + # Verify requests using robust assert_has_calls matching + # mock.ANY for base64 zipped Filesystem + request_mock.assert_has_calls([ + mock.call( + "post", + "skills?skillId=test-skill", { - "skillName": ( - "projects/test-project/locations/test-location/skills/skill-1" - ), - "description": "Skill 1 Description", - } - ] - } - - with mock.patch.object( - async_skills_client._api_client, "async_request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(mock_retrieve_response) - ) - - response = await async_skills_client.retrieve( - query="test query", config={"top_k": 1} - ) - - assert isinstance(response, genai.types.RetrieveSkillsResponse) - assert len(response.retrieved_skills) == 1 - assert response.retrieved_skills[0].skill_name == ( - "projects/test-project/locations/test-location/skills/skill-1" - ) - - request_mock.assert_called_once_with( - "get", - "skills:retrieve?query=test+query&topK=1", - {"_query": {"query": "test query", "topK": 1}}, - None, - ) - - def test_create_skill(self, skills_client): - """Tests the create_skill method with wait_for_completion=True.""" - with tempfile.TemporaryDirectory() as tmpdir: - # Create a dummy file in tmpdir - with open(os.path.join(tmpdir, "SKILL.md"), "w") as f: - f.write("# Test Skill") - - # Prepare mock responses - pending_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" - ), - "done": False, - } - finished_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" - ), - "done": True, - "response": { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill" - ), - "displayName": "My Test Skill", - "description": "My Test Skill Description", - }, - } - - # Final Skill response returned by get call - skill_response = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill" - ), - "displayName": "My Test Skill", - "description": "My Test Skill Description", - } - - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.side_effect = [ - genai_types.HttpResponse(body=json.dumps(pending_op)), - genai_types.HttpResponse(body=json.dumps(finished_op)), - genai_types.HttpResponse(body=json.dumps(skill_response)), - ] - - # We mock time.sleep to speed up the test - with mock.patch.object(time, "sleep", return_value=None): - skill = skills_client.create( - display_name="My Test Skill", - description="My Test Skill Description", - config={"local_path": tmpdir, "wait_for_completion": True}, - ) - - # Verify requests using robust assert_has_calls matching - # mock.ANY for base64 zipped Filesystem - request_mock.assert_has_calls( - [ - mock.call( - "post", - "skills", - { - "displayName": "My Test Skill", - "description": "My Test Skill Description", - "zippedFilesystem": mock.ANY, - }, - None, - ), - mock.call( - "get", - "projects/test-project/locations/test-location/skills/test-skill/operations/op-123", - { - "_url": { - "operationName": "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" - } - }, - None, - ), - mock.call( - "get", - "projects/test-project/locations/test-location/skills/test-skill", - { - "_url": { - "name": "projects/test-project/locations/test-location/skills/test-skill" - } - }, - None, - ), - ] - ) - - # Verify returned skill - assert isinstance(skill, genai.types.Skill) - assert ( - skill.name - == "projects/test-project/locations/test-location/skills/test-skill" - ) - assert skill.display_name == "My Test Skill" - assert skill.description == "My Test Skill Description" - - def test_create_skill_no_wait(self, skills_client): - """Tests the create_skill method with wait_for_completion=False.""" - with tempfile.TemporaryDirectory() as tmpdir: - with open(os.path.join(tmpdir, "SKILL.md"), "w") as f: - f.write("# Test Skill") - - pending_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" - ), - "done": False, - } - - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(pending_op) - ) - - operation = skills_client.create( - display_name="My Test Skill", - description="My Test Skill Description", - config={"local_path": tmpdir, "wait_for_completion": False}, - ) - - # Assertions - assert request_mock.call_count == 1 - assert isinstance(operation, genai.types.SkillOperation) - assert ( - operation.name - == "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" - ) - assert not operation.done - - @pytest.mark.asyncio - async def test_create_skill_async(self, async_skills_client): - """Tests the create_skill method asynchronously with wait_for_completion=True.""" - with tempfile.TemporaryDirectory() as tmpdir: - with open(os.path.join(tmpdir, "SKILL.md"), "w") as f: - f.write("# Test Skill") - - pending_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" - ), - "done": False, - } - finished_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" - ), - "done": True, - "response": { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill" - ), "displayName": "My Test Skill", "description": "My Test Skill Description", + "zippedFilesystem": mock.ANY, + "_query": {"skillId": "test-skill"}, }, - } - - # Final Skill response returned by async get call - skill_response = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill" - ), - "displayName": "My Test Skill", - "description": "My Test Skill Description", - } - - with mock.patch.object( - async_skills_client._api_client, "async_request", autospec=True - ) as request_mock: - request_mock.side_effect = [ - genai_types.HttpResponse(body=json.dumps(pending_op)), - genai_types.HttpResponse(body=json.dumps(finished_op)), - genai_types.HttpResponse(body=json.dumps(skill_response)), - ] - - with mock.patch.object(asyncio, "sleep", new_callable=mock.AsyncMock): - skill = await async_skills_client.create( - display_name="My Test Skill", - description="My Test Skill Description", - config={"local_path": tmpdir, "wait_for_completion": True}, - ) - - # Verify requests using robust assert_has_calls - request_mock.assert_has_calls( - [ - mock.call( - "post", - "skills", - { - "displayName": "My Test Skill", - "description": "My Test Skill Description", - "zippedFilesystem": mock.ANY, - }, - None, - ), - mock.call( - "get", - "projects/test-project/locations/test-location/skills/test-skill/operations/op-123", - { - "_url": { - "operationName": "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" - } - }, - None, - ), - mock.call( - "get", - "projects/test-project/locations/test-location/skills/test-skill", - { - "_url": { - "name": "projects/test-project/locations/test-location/skills/test-skill" - } - }, - None, - ), - ] - ) - - # Verify returned skill - assert isinstance(skill, genai.types.Skill) - assert ( - skill.name - == "projects/test-project/locations/test-location/skills/test-skill" - ) - assert skill.display_name == "My Test Skill" - assert skill.description == "My Test Skill Description" - - def test_update_skill(self, skills_client): - """Tests the update method with wait_for_completion=True (default).""" - skill_name = "projects/test-project/locations/test-location/skills/test-skill" - # Prepare mock responses - pending_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" - ), - "done": False, - } - finished_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" - ), - "done": True, - "response": { - "name": skill_name, - "displayName": "Updated Skill", - "description": "Updated Description", - }, - } - skill_response = { - "name": skill_name, - "displayName": "Updated Skill", - "description": "Updated Description", - } - - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.side_effect = [ - genai_types.HttpResponse(body=json.dumps(pending_op)), - genai_types.HttpResponse(body=json.dumps(finished_op)), - genai_types.HttpResponse(body=json.dumps(skill_response)), - ] - - with mock.patch.object(time, "sleep", return_value=None): - skill = skills_client.update( - name=skill_name, - config={ - "display_name": "Updated Skill", - "description": "Updated Description", - }, - ) - - # Verify requests using robust assert_has_calls - request_mock.assert_has_calls( - [ - mock.call( - "patch", - f"{skill_name}?updateMask=displayName%2Cdescription", - { - "_url": { - "name": "projects/test-project/locations/test-location/skills/test-skill" - }, - "displayName": "Updated Skill", - "description": "Updated Description", - "_query": { - "updateMask": "displayName,description", - }, - }, - None, - ), - mock.call( - "get", - "projects/test-project/locations/test-location/skills/test-skill/operations/op-456", - { - "_url": { - "operationName": "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" - } - }, - None, - ), - mock.call( - "get", - skill_name, - { - "_url": { - "name": "projects/test-project/locations/test-location/skills/test-skill" - } - }, - None, - ), - ] - ) - - # Verify returned skill - assert isinstance(skill, genai.types.Skill) - assert skill.name == skill_name - assert skill.display_name == "Updated Skill" - assert skill.description == "Updated Description" - - def test_update_skill_no_wait(self, skills_client): - """Tests the update method with wait_for_completion=False.""" - skill_name = "projects/test-project/locations/test-location/skills/test-skill" - pending_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" + None, ), - "done": False, - } - - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(pending_op) - ) - - operation = skills_client.update( - name=skill_name, - config={ - "display_name": "Updated Skill", - "wait_for_completion": False, + mock.call( + "get", + "projects/test-project/locations/test-location/skills/test-skill/operations/op-123", + { + "_url": { + "operationName": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" + ) + } }, - ) - - # Assertions - assert isinstance(operation, genai.types.SkillOperation) - assert ( - operation.name - == "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" - ) - assert not operation.done - - request_mock.assert_called_once_with( - "patch", - f"{skill_name}?updateMask=displayName", + None, + ), + mock.call( + "get", + "projects/test-project/locations/test-location/skills/test-skill", { "_url": { "name": ( "projects/test-project/locations/test-location/skills/test-skill" ) - }, - "displayName": "Updated Skill", - "_query": { - "updateMask": "displayName", - }, + } }, None, - ) - - @pytest.mark.asyncio - async def test_update_skill_async(self, async_skills_client): - """Tests the async update method with wait_for_completion=True.""" - skill_name = "projects/test-project/locations/test-location/skills/test-skill" - pending_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" - ), - "done": False, - } - finished_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" - ), - "done": True, - "response": { - "name": skill_name, - "displayName": "Updated Skill", - }, - } - skill_response = { - "name": skill_name, - "displayName": "Updated Skill", - } - - with mock.patch.object( - async_skills_client._api_client, "async_request", autospec=True - ) as request_mock: - request_mock.side_effect = [ - genai_types.HttpResponse(body=json.dumps(pending_op)), - genai_types.HttpResponse(body=json.dumps(finished_op)), - genai_types.HttpResponse(body=json.dumps(skill_response)), - ] - - with mock.patch.object(asyncio, "sleep", new_callable=mock.AsyncMock): - skill = await async_skills_client.update( - name=skill_name, - config={ - "display_name": "Updated Skill", - "wait_for_completion": True, - }, - ) - - # Verify requests using robust assert_has_calls - request_mock.assert_has_calls( - [ - mock.call( - "patch", - f"{skill_name}?updateMask=displayName", - { - "_url": { - "name": "projects/test-project/locations/test-location/skills/test-skill" - }, - "displayName": "Updated Skill", - "_query": { - "updateMask": "displayName", - }, - }, - None, - ), - mock.call( - "get", - "projects/test-project/locations/test-location/skills/test-skill/operations/op-456", - { - "_url": { - "operationName": "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" - } - }, - None, - ), - mock.call( - "get", - skill_name, - { - "_url": { - "name": "projects/test-project/locations/test-location/skills/test-skill" - } - }, - None, - ), - ] - ) - - # Verify returned skill - assert isinstance(skill, genai.types.Skill) - assert skill.name == skill_name - assert skill.display_name == "Updated Skill" - - def test_update_skill_invalid_args(self, skills_client): - """Verifies ValueError is raised when no update fields are provided.""" - skill_name = "projects/test-project/locations/test-location/skills/test-skill" - with pytest.raises( - ValueError, - match=( - "At least one of `display_name`, `description`, `local_path`, or" - " `zipped_filesystem` must be provided for update in config." ), - ): - skills_client.update(name=skill_name) - - def test_update_skill_mutually_exclusive_args(self, skills_client): - """Verifies ValueError is raised when both local_path and zipped_filesystem are provided.""" - skill_name = "projects/test-project/locations/test-location/skills/test-skill" - with pytest.raises( - ValueError, - match="Only one of `local_path` or `zipped_filesystem` can be provided", - ): - skills_client.update( - name=skill_name, - config={ - "local_path": "/some/path", - "zipped_filesystem": b"zipped_bytes", - }, - ) - - def test_list_skills(self, skills_client): - """Tests the list method using the standard Pager.""" - mock_list_response = { - "skills": [ - { - "name": ( - "projects/test-project/locations/test-location/skills/skill-1" - ), - "displayName": "Skill 1", - }, - { - "name": ( - "projects/test-project/locations/test-location/skills/skill-2" - ), - "displayName": "Skill 2", - }, - ], - "nextPageToken": "token-123", - } - mock_list_response_page_2 = { - "skills": [ + ]) + + # Verify returned skill + assert isinstance(skill, genai.types.Skill) + assert ( + skill.name + == "projects/test-project/locations/test-location/skills/test-skill" + ) + assert skill.display_name == "My Test Skill" + assert skill.description == "My Test Skill Description" + + def test_create_skill_no_wait(self, skills_client): + """Tests the create_skill method with wait_for_completion=False.""" + with tempfile.TemporaryDirectory() as tmpdir: + with open(os.path.join(tmpdir, "SKILL.md"), "w") as f: + f.write("# Test Skill") + + pending_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" + ), + "done": False, + } + + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(pending_op) + ) + + operation = skills_client.create( + skill_id="test-skill", + display_name="My Test Skill", + description="My Test Skill Description", + config={"local_path": tmpdir, "wait_for_completion": False}, + ) + + # Assertions + assert request_mock.call_count == 1 + assert isinstance(operation, genai.types.SkillOperation) + assert ( + operation.name + == "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" + ) + assert not operation.done + + @pytest.mark.asyncio + async def test_create_skill_async(self, async_skills_client): + """Tests the create_skill method asynchronously with wait_for_completion=True.""" + with tempfile.TemporaryDirectory() as tmpdir: + with open(os.path.join(tmpdir, "SKILL.md"), "w") as f: + f.write("# Test Skill") + + pending_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" + ), + "done": False, + } + finished_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" + ), + "done": True, + "response": { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill" + ), + "displayName": "My Test Skill", + "description": "My Test Skill Description", + }, + } + + # Final Skill response returned by async get call + skill_response = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill" + ), + "displayName": "My Test Skill", + "description": "My Test Skill Description", + } + + with mock.patch.object( + async_skills_client._api_client, "async_request", autospec=True + ) as request_mock: + request_mock.side_effect = [ + genai_types.HttpResponse(body=json.dumps(pending_op)), + genai_types.HttpResponse(body=json.dumps(finished_op)), + genai_types.HttpResponse(body=json.dumps(skill_response)), + ] + + with mock.patch.object(asyncio, "sleep", new_callable=mock.AsyncMock): + skill = await async_skills_client.create( + skill_id="test-skill", + display_name="My Test Skill", + description="My Test Skill Description", + config={"local_path": tmpdir, "wait_for_completion": True}, + ) + + # Verify requests using robust assert_has_calls + request_mock.assert_has_calls([ + mock.call( + "post", + "skills?skillId=test-skill", { - "name": ( - "projects/test-project/locations/test-location/skills/skill-3" - ), - "displayName": "Skill 3", + "displayName": "My Test Skill", + "description": "My Test Skill Description", + "zippedFilesystem": mock.ANY, + "_query": {"skillId": "test-skill"}, }, - ], - } - - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.side_effect = [ - genai_types.HttpResponse(body=json.dumps(mock_list_response)), - genai_types.HttpResponse(body=json.dumps(mock_list_response_page_2)), - ] - - skills = list(skills_client.list()) - - # Verify Pager correct retrieval across pages - assert len(skills) == 3 - assert skills[0].display_name == "Skill 1" - assert skills[1].display_name == "Skill 2" - assert skills[2].display_name == "Skill 3" - - # Verify requests using robust assert_has_calls - request_mock.assert_has_calls( - [ - mock.call( - "get", - "skills", - {}, - None, - ), - mock.call( - "get", - "skills?pageToken=token-123", - {"_query": {"pageToken": "token-123"}}, - None, - ), - ] - ) - - @pytest.mark.asyncio - async def test_list_skills_async(self, async_skills_client): - """Tests the async list method returning AsyncPager.""" - mock_list_response = { - "skills": [ + None, + ), + mock.call( + "get", + "projects/test-project/locations/test-location/skills/test-skill/operations/op-123", { - "name": ( - "projects/test-project/locations/test-location/skills/skill-1" - ), - "displayName": "Skill 1", + "_url": { + "operationName": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-123" + ) + } }, - ], - } - - with mock.patch.object( - async_skills_client._api_client, "async_request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(mock_list_response) - ) - - skills = [] - pager = await async_skills_client.list() - async for skill in pager: - skills.append(skill) - - assert len(skills) == 1 - assert skills[0].display_name == "Skill 1" - request_mock.assert_called_once_with( - "get", - "skills", - {}, None, - ) - - def test_delete_skill(self, skills_client): - """Tests the delete method with wait_for_completion=True (default).""" - skill_name = "projects/test-project/locations/test-location/skills/test-skill" - pending_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" - ), - "done": False, - } - finished_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" - ), - "done": True, - "response": {}, - } - - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.side_effect = [ - genai_types.HttpResponse(body=json.dumps(pending_op)), - genai_types.HttpResponse(body=json.dumps(finished_op)), - ] - - with mock.patch("time.sleep", return_value=None): - result = skills_client.delete(name=skill_name) - - assert result is None - - # Verify both DELETE and LRO GET requests using robust assert_has_calls - request_mock.assert_has_calls( - [ - mock.call( - "delete", - skill_name, - {"_url": {"name": skill_name}}, - None, - ), - mock.call( - "get", - "projects/test-project/locations/test-location/skills/test-skill/operations/op-789", - { - "_url": { - "operationName": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" - ) - } - }, - None, - ), - ] - ) - - def test_delete_skill_no_wait(self, skills_client): - """Tests the delete method with wait_for_completion=False.""" - skill_name = "projects/test-project/locations/test-location/skills/test-skill" - pending_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" ), - "done": False, - } - - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(pending_op) - ) - - operation = skills_client.delete( - name=skill_name, config={"wait_for_completion": False} - ) - - assert isinstance(operation, genai.types.DeleteSkillOperation) - assert ( - operation.name - == "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" - ) - assert not operation.done - request_mock.assert_called_once_with( - "delete", - skill_name, - {"_url": {"name": skill_name}}, + mock.call( + "get", + "projects/test-project/locations/test-location/skills/test-skill", + { + "_url": { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + } + }, None, - ) - - @pytest.mark.asyncio - async def test_delete_skill_async(self, async_skills_client): - """Tests the async delete method with wait_for_completion=True.""" - skill_name = "projects/test-project/locations/test-location/skills/test-skill" - pending_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" ), - "done": False, - } - finished_op = { - "name": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" - ), - "done": True, - "response": {}, - } - - with mock.patch.object( - async_skills_client._api_client, "async_request", autospec=True - ) as request_mock: - request_mock.side_effect = [ - genai_types.HttpResponse(body=json.dumps(pending_op)), - genai_types.HttpResponse(body=json.dumps(finished_op)), - ] - - with mock.patch("asyncio.sleep", new_callable=mock.AsyncMock): - result = await async_skills_client.delete( - name=skill_name, config={"wait_for_completion": True} - ) - - assert result is None - - # Verify both DELETE and LRO GET requests asynchronously using robust assert_has_calls - request_mock.assert_has_calls( - [ - mock.call( - "delete", - skill_name, - {"_url": {"name": skill_name}}, - None, - ), - mock.call( - "get", - "projects/test-project/locations/test-location/skills/test-skill/operations/op-789", - { - "_url": { - "operationName": ( - "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" - ) - } - }, - None, - ), - ] - ) - - def test_get_skill_revision(self, skills_client): - revision_name = "projects/test-project/locations/test-location/skills/test-skill/revisions/rev-1" - mock_response = { - "name": revision_name, - "state": "ACTIVE", - } + ]) + + # Verify returned skill + assert isinstance(skill, genai.types.Skill) + assert ( + skill.name + == "projects/test-project/locations/test-location/skills/test-skill" + ) + assert skill.display_name == "My Test Skill" + assert skill.description == "My Test Skill Description" + + def test_update_skill(self, skills_client): + """Tests the update method with wait_for_completion=True (default).""" + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + # Prepare mock responses + pending_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" + ), + "done": False, + } + finished_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" + ), + "done": True, + "response": { + "name": skill_name, + "displayName": "Updated Skill", + "description": "Updated Description", + }, + } + skill_response = { + "name": skill_name, + "displayName": "Updated Skill", + "description": "Updated Description", + } + + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.side_effect = [ + genai_types.HttpResponse(body=json.dumps(pending_op)), + genai_types.HttpResponse(body=json.dumps(finished_op)), + genai_types.HttpResponse(body=json.dumps(skill_response)), + ] + + with mock.patch.object(time, "sleep", return_value=None): + skill = skills_client.update( + name=skill_name, + config={ + "display_name": "Updated Skill", + "description": "Updated Description", + }, + ) + + # Verify requests using robust assert_has_calls + request_mock.assert_has_calls([ + mock.call( + "patch", + f"{skill_name}?updateMask=displayName%2Cdescription", + { + "_url": { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + }, + "displayName": "Updated Skill", + "description": "Updated Description", + "_query": { + "updateMask": "displayName,description", + }, + }, + None, + ), + mock.call( + "get", + "projects/test-project/locations/test-location/skills/test-skill/operations/op-456", + { + "_url": { + "operationName": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" + ) + } + }, + None, + ), + mock.call( + "get", + skill_name, + { + "_url": { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + } + }, + None, + ), + ]) + + # Verify returned skill + assert isinstance(skill, genai.types.Skill) + assert skill.name == skill_name + assert skill.display_name == "Updated Skill" + assert skill.description == "Updated Description" + + def test_update_skill_no_wait(self, skills_client): + """Tests the update method with wait_for_completion=False.""" + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + pending_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" + ), + "done": False, + } - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(mock_response) - ) + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(pending_op) + ) + + operation = skills_client.update( + name=skill_name, + config={ + "display_name": "Updated Skill", + "wait_for_completion": False, + }, + ) + + # Assertions + assert isinstance(operation, genai.types.SkillOperation) + assert ( + operation.name + == "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" + ) + assert not operation.done + + request_mock.assert_called_once_with( + "patch", + f"{skill_name}?updateMask=displayName", + { + "_url": { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + }, + "displayName": "Updated Skill", + "_query": { + "updateMask": "displayName", + }, + }, + None, + ) + + @pytest.mark.asyncio + async def test_update_skill_async(self, async_skills_client): + """Tests the async update method with wait_for_completion=True.""" + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + pending_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" + ), + "done": False, + } + finished_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" + ), + "done": True, + "response": { + "name": skill_name, + "displayName": "Updated Skill", + }, + } + skill_response = { + "name": skill_name, + "displayName": "Updated Skill", + } - revision = skills_client.revisions.get(name=revision_name) + with mock.patch.object( + async_skills_client._api_client, "async_request", autospec=True + ) as request_mock: + request_mock.side_effect = [ + genai_types.HttpResponse(body=json.dumps(pending_op)), + genai_types.HttpResponse(body=json.dumps(finished_op)), + genai_types.HttpResponse(body=json.dumps(skill_response)), + ] + + with mock.patch.object(asyncio, "sleep", new_callable=mock.AsyncMock): + skill = await async_skills_client.update( + name=skill_name, + config={ + "display_name": "Updated Skill", + "wait_for_completion": True, + }, + ) + + # Verify requests using robust assert_has_calls + request_mock.assert_has_calls([ + mock.call( + "patch", + f"{skill_name}?updateMask=displayName", + { + "_url": { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + }, + "displayName": "Updated Skill", + "_query": { + "updateMask": "displayName", + }, + }, + None, + ), + mock.call( + "get", + "projects/test-project/locations/test-location/skills/test-skill/operations/op-456", + { + "_url": { + "operationName": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-456" + ) + } + }, + None, + ), + mock.call( + "get", + skill_name, + { + "_url": { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + } + }, + None, + ), + ]) + + # Verify returned skill + assert isinstance(skill, genai.types.Skill) + assert skill.name == skill_name + assert skill.display_name == "Updated Skill" + + def test_update_skill_invalid_args(self, skills_client): + """Verifies ValueError is raised when no update fields are provided.""" + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + with pytest.raises( + ValueError, + match=( + "At least one of `display_name`, `description`, `local_path`, or" + " `zipped_filesystem` must be provided for update in config." + ), + ): + skills_client.update(name=skill_name) + + def test_update_skill_mutually_exclusive_args(self, skills_client): + """Verifies ValueError is raised when both local_path and zipped_filesystem are provided.""" + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + with pytest.raises( + ValueError, + match="Only one of `local_path` or `zipped_filesystem` can be provided", + ): + skills_client.update( + name=skill_name, + config={ + "local_path": "/some/path", + "zipped_filesystem": b"zipped_bytes", + }, + ) + + def test_list_skills(self, skills_client): + """Tests the list method using the standard Pager.""" + mock_list_response = { + "skills": [ + { + "name": ( + "projects/test-project/locations/test-location/skills/skill-1" + ), + "displayName": "Skill 1", + }, + { + "name": ( + "projects/test-project/locations/test-location/skills/skill-2" + ), + "displayName": "Skill 2", + }, + ], + "nextPageToken": "token-123", + } + mock_list_response_page_2 = { + "skills": [ + { + "name": ( + "projects/test-project/locations/test-location/skills/skill-3" + ), + "displayName": "Skill 3", + }, + ], + } - assert isinstance(revision, genai.types.SkillRevision) - assert revision.name == revision_name - assert revision.state == "ACTIVE" + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.side_effect = [ + genai_types.HttpResponse(body=json.dumps(mock_list_response)), + genai_types.HttpResponse(body=json.dumps(mock_list_response_page_2)), + ] + + skills = list(skills_client.list()) + + # Verify Pager correct retrieval across pages + assert len(skills) == 3 + assert skills[0].display_name == "Skill 1" + assert skills[1].display_name == "Skill 2" + assert skills[2].display_name == "Skill 3" + + # Verify requests using robust assert_has_calls + request_mock.assert_has_calls([ + mock.call( + "get", + "skills", + {}, + None, + ), + mock.call( + "get", + "skills?pageToken=token-123", + {"_query": {"pageToken": "token-123"}}, + None, + ), + ]) + + @pytest.mark.asyncio + async def test_list_skills_async(self, async_skills_client): + """Tests the async list method returning AsyncPager.""" + mock_list_response = { + "skills": [ + { + "name": ( + "projects/test-project/locations/test-location/skills/skill-1" + ), + "displayName": "Skill 1", + }, + ], + } - request_mock.assert_called_once_with( - "get", - revision_name, - {"_url": {"name": revision_name}}, - None, - ) + with mock.patch.object( + async_skills_client._api_client, "async_request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(mock_list_response) + ) + + skills = [] + pager = await async_skills_client.list() + async for skill in pager: + skills.append(skill) + + assert len(skills) == 1 + assert skills[0].display_name == "Skill 1" + request_mock.assert_called_once_with( + "get", + "skills", + {}, + None, + ) + + def test_delete_skill(self, skills_client): + """Tests the delete method with wait_for_completion=True (default).""" + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + pending_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" + ), + "done": False, + } + finished_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" + ), + "done": True, + "response": {}, + } - @pytest.mark.asyncio - async def test_get_skill_revision_async(self, async_skills_client): - revision_name = "projects/test-project/locations/test-location/skills/test-skill/revisions/rev-1" - mock_response = { - "name": revision_name, - "state": "ACTIVE", - } + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.side_effect = [ + genai_types.HttpResponse(body=json.dumps(pending_op)), + genai_types.HttpResponse(body=json.dumps(finished_op)), + ] + + with mock.patch("time.sleep", return_value=None): + result = skills_client.delete(name=skill_name) + + assert result is None + + # Verify both DELETE and LRO GET requests using robust assert_has_calls + request_mock.assert_has_calls([ + mock.call( + "delete", + skill_name, + {"_url": {"name": skill_name}}, + None, + ), + mock.call( + "get", + "projects/test-project/locations/test-location/skills/test-skill/operations/op-789", + { + "_url": { + "operationName": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" + ) + } + }, + None, + ), + ]) + + def test_delete_skill_no_wait(self, skills_client): + """Tests the delete method with wait_for_completion=False.""" + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + pending_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" + ), + "done": False, + } - with mock.patch.object( - async_skills_client._api_client, "async_request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(mock_response) - ) + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(pending_op) + ) + + operation = skills_client.delete( + name=skill_name, config={"wait_for_completion": False} + ) + + assert isinstance(operation, genai.types.DeleteSkillOperation) + assert ( + operation.name + == "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" + ) + assert not operation.done + request_mock.assert_called_once_with( + "delete", + skill_name, + {"_url": {"name": skill_name}}, + None, + ) + + @pytest.mark.asyncio + async def test_delete_skill_async(self, async_skills_client): + """Tests the async delete method with wait_for_completion=True.""" + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + pending_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" + ), + "done": False, + } + finished_op = { + "name": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" + ), + "done": True, + "response": {}, + } - revision = await async_skills_client.revisions.get(name=revision_name) + with mock.patch.object( + async_skills_client._api_client, "async_request", autospec=True + ) as request_mock: + request_mock.side_effect = [ + genai_types.HttpResponse(body=json.dumps(pending_op)), + genai_types.HttpResponse(body=json.dumps(finished_op)), + ] + + with mock.patch("asyncio.sleep", new_callable=mock.AsyncMock): + result = await async_skills_client.delete( + name=skill_name, config={"wait_for_completion": True} + ) + + assert result is None + + # Verify both DELETE and LRO GET requests asynchronously using robust assert_has_calls + request_mock.assert_has_calls([ + mock.call( + "delete", + skill_name, + {"_url": {"name": skill_name}}, + None, + ), + mock.call( + "get", + "projects/test-project/locations/test-location/skills/test-skill/operations/op-789", + { + "_url": { + "operationName": ( + "projects/test-project/locations/test-location/skills/test-skill/operations/op-789" + ) + } + }, + None, + ), + ]) + + def test_get_skill_revision(self, skills_client): + revision_name = "projects/test-project/locations/test-location/skills/test-skill/revisions/rev-1" + mock_response = { + "name": revision_name, + "state": "ACTIVE", + } - assert isinstance(revision, genai.types.SkillRevision) - assert revision.name == revision_name - assert revision.state == "ACTIVE" + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(mock_response) + ) + + revision = skills_client.revisions.get(name=revision_name) + + assert isinstance(revision, genai.types.SkillRevision) + assert revision.name == revision_name + assert revision.state == "ACTIVE" + + request_mock.assert_called_once_with( + "get", + revision_name, + {"_url": {"name": revision_name}}, + None, + ) + + @pytest.mark.asyncio + async def test_get_skill_revision_async(self, async_skills_client): + revision_name = "projects/test-project/locations/test-location/skills/test-skill/revisions/rev-1" + mock_response = { + "name": revision_name, + "state": "ACTIVE", + } - request_mock.assert_called_once_with( - "get", - revision_name, - {"_url": {"name": revision_name}}, - None, - ) + with mock.patch.object( + async_skills_client._api_client, "async_request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(mock_response) + ) + + revision = await async_skills_client.revisions.get(name=revision_name) + + assert isinstance(revision, genai.types.SkillRevision) + assert revision.name == revision_name + assert revision.state == "ACTIVE" + + request_mock.assert_called_once_with( + "get", + revision_name, + {"_url": {"name": revision_name}}, + None, + ) + + def test_list_skill_revisions(self, skills_client): + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + mock_response = { + "skillRevisions": [{ + "name": f"{skill_name}/revisions/rev-1", + "state": "ACTIVE", + }] + } - def test_list_skill_revisions(self, skills_client): - skill_name = "projects/test-project/locations/test-location/skills/test-skill" - mock_response = { - "skillRevisions": [ - { - "name": f"{skill_name}/revisions/rev-1", - "state": "ACTIVE", - } - ] - } - - with mock.patch.object( - skills_client._api_client, "request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(mock_response) - ) - - response = skills_client.revisions.list(name=skill_name) - - assert isinstance(response, genai.types.ListSkillRevisionsResponse) - assert len(response.skill_revisions) == 1 - assert response.skill_revisions[0].name == f"{skill_name}/revisions/rev-1" - - request_mock.assert_called_once_with( - "get", - f"{skill_name}/revisions", - {"_url": {"name": skill_name}}, - None, - ) + with mock.patch.object( + skills_client._api_client, "request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(mock_response) + ) + + response = skills_client.revisions.list(name=skill_name) + + assert isinstance(response, genai.types.ListSkillRevisionsResponse) + assert len(response.skill_revisions) == 1 + assert response.skill_revisions[0].name == f"{skill_name}/revisions/rev-1" + + request_mock.assert_called_once_with( + "get", + f"{skill_name}/revisions", + {"_url": {"name": skill_name}}, + None, + ) + + @pytest.mark.asyncio + async def test_list_skill_revisions_async(self, async_skills_client): + skill_name = ( + "projects/test-project/locations/test-location/skills/test-skill" + ) + mock_response = { + "skillRevisions": [{ + "name": f"{skill_name}/revisions/rev-1", + "state": "ACTIVE", + }] + } - @pytest.mark.asyncio - async def test_list_skill_revisions_async(self, async_skills_client): - skill_name = "projects/test-project/locations/test-location/skills/test-skill" - mock_response = { - "skillRevisions": [ - { - "name": f"{skill_name}/revisions/rev-1", - "state": "ACTIVE", - } - ] - } - - with mock.patch.object( - async_skills_client._api_client, "async_request", autospec=True - ) as request_mock: - request_mock.return_value = genai_types.HttpResponse( - body=json.dumps(mock_response) - ) - - response = await async_skills_client.revisions.list(name=skill_name) - - assert isinstance(response, genai.types.ListSkillRevisionsResponse) - assert len(response.skill_revisions) == 1 - assert response.skill_revisions[0].name == f"{skill_name}/revisions/rev-1" - - request_mock.assert_called_once_with( - "get", - f"{skill_name}/revisions", - {"_url": {"name": skill_name}}, - None, - ) + with mock.patch.object( + async_skills_client._api_client, "async_request", autospec=True + ) as request_mock: + request_mock.return_value = genai_types.HttpResponse( + body=json.dumps(mock_response) + ) + + response = await async_skills_client.revisions.list(name=skill_name) + + assert isinstance(response, genai.types.ListSkillRevisionsResponse) + assert len(response.skill_revisions) == 1 + assert response.skill_revisions[0].name == f"{skill_name}/revisions/rev-1" + + request_mock.assert_called_once_with( + "get", + f"{skill_name}/revisions", + {"_url": {"name": skill_name}}, + None, + )