Skip to content

Commit e4fdb7a

Browse files
vertex-sdk-botcopybara-github
authored andcommitted
feat: Add GetSkillRevision and ListSkillRevisions methods in Vertex AI Skill Registry SDK
PiperOrigin-RevId: 913847550
1 parent ea87edc commit e4fdb7a

7 files changed

Lines changed: 841 additions & 2 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
"""Tests the skills.revisions methods against the autopush endpoint."""
16+
17+
from tests.unit.vertexai.genai.replays import pytest_helper
18+
from vertexai._genai import types
19+
20+
21+
def test_get_skill_revision(client, tmp_path):
22+
# Target the autopush sandbox endpoint for the Skill Registry API
23+
client._api_client._http_options.base_url = (
24+
"https://us-central1-autopush-aiplatform.sandbox.googleapis.com"
25+
)
26+
27+
# 1. Create a fresh unique skill
28+
with open(tmp_path / "SKILL.md", "w") as f:
29+
f.write("# Replay Revision Test Skill\nThis is a test skill.")
30+
31+
created_skill = client.skills.create(
32+
display_name="Replay Revision Test Skill",
33+
description="A temporary skill to test revisions E2E",
34+
config=types.CreateSkillConfig(
35+
local_path=str(tmp_path), wait_for_completion=True
36+
),
37+
)
38+
39+
try:
40+
assert created_skill.name is not None
41+
42+
# 2. List revisions to dynamically discover the revision ID
43+
revisions_response = client.skills.revisions.list(name=created_skill.name)
44+
revisions_list = revisions_response.skill_revisions
45+
46+
assert len(revisions_list) > 0
47+
first_revision = revisions_list[0]
48+
assert isinstance(first_revision, types.SkillRevision)
49+
assert first_revision.name is not None
50+
51+
# 3. Explicitly GET the revision by its resource name
52+
revision = client.skills.revisions.get(name=first_revision.name)
53+
54+
assert isinstance(revision, types.SkillRevision)
55+
assert revision.name == first_revision.name
56+
assert revision.state == types.SkillState.ACTIVE
57+
58+
finally:
59+
# 4. Clean up the temporary skill
60+
client.skills.delete(
61+
name=created_skill.name,
62+
config=types.DeleteSkillConfig(wait_for_completion=True),
63+
)
64+
65+
66+
pytestmark = pytest_helper.setup(
67+
file=__file__,
68+
globals_for_file=globals(),
69+
test_method="skills.revisions.get",
70+
)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
"""Tests the skills.revisions.list() method against the autopush endpoint."""
16+
17+
from tests.unit.vertexai.genai.replays import pytest_helper
18+
from vertexai._genai import types
19+
20+
21+
def test_list_skill_revisions(client, tmp_path):
22+
# Target the autopush sandbox endpoint for the Skill Registry API
23+
client._api_client._http_options.base_url = (
24+
"https://us-central1-autopush-aiplatform.sandbox.googleapis.com"
25+
)
26+
27+
# 1. Create a fresh unique skill
28+
with open(tmp_path / "SKILL.md", "w") as f:
29+
f.write("# Replay List Revisions Test Skill\nThis is a test skill.")
30+
31+
created_skill = client.skills.create(
32+
display_name="Replay List Revisions Test Skill",
33+
description="A temporary skill to test list revisions E2E",
34+
config=types.CreateSkillConfig(
35+
local_path=str(tmp_path), wait_for_completion=True
36+
),
37+
)
38+
39+
try:
40+
assert created_skill.name is not None
41+
42+
# 2. List revisions
43+
revisions_response = client.skills.revisions.list(name=created_skill.name)
44+
revisions_list = revisions_response.skill_revisions
45+
46+
assert len(revisions_list) > 0
47+
first_revision = revisions_list[0]
48+
assert isinstance(first_revision, types.SkillRevision)
49+
assert first_revision.name is not None
50+
assert first_revision.state == types.SkillState.ACTIVE
51+
52+
finally:
53+
# 3. Clean up the temporary skill
54+
client.skills.delete(
55+
name=created_skill.name,
56+
config=types.DeleteSkillConfig(wait_for_completion=True),
57+
)
58+
59+
60+
pytestmark = pytest_helper.setup(
61+
file=__file__,
62+
globals_for_file=globals(),
63+
test_method="skills.revisions.list",
64+
)

tests/unit/vertexai/genai/test_genai_skills.py

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ def async_skills_client():
4747

4848

4949
class TestGenaiSkills:
50-
"""Tests the Genai Skills client."""
5150

5251
mock_get_skill_response = {
5352
"name": "projects/test-project/locations/test-location/skills/test-skill",
@@ -219,7 +218,8 @@ def test_create_skill(self, skills_client):
219218
config={"local_path": tmpdir, "wait_for_completion": True},
220219
)
221220

222-
# Verify requests using robust assert_has_calls matching mock.ANY for base64 zipped Filesystem
221+
# Verify requests using robust assert_has_calls matching
222+
# mock.ANY for base64 zipped Filesystem
223223
request_mock.assert_has_calls(
224224
[
225225
mock.call(
@@ -896,3 +896,121 @@ async def test_delete_skill_async(self, async_skills_client):
896896
),
897897
]
898898
)
899+
900+
def test_get_skill_revision(self, skills_client):
901+
revision_name = "projects/test-project/locations/test-location/skills/test-skill/revisions/rev-1"
902+
mock_response = {
903+
"name": revision_name,
904+
"state": "ACTIVE",
905+
}
906+
907+
with mock.patch.object(
908+
skills_client._api_client, "request", autospec=True
909+
) as request_mock:
910+
request_mock.return_value = genai_types.HttpResponse(
911+
body=json.dumps(mock_response)
912+
)
913+
914+
revision = skills_client.revisions.get(name=revision_name)
915+
916+
assert isinstance(revision, genai.types.SkillRevision)
917+
assert revision.name == revision_name
918+
assert revision.state == "ACTIVE"
919+
920+
request_mock.assert_called_once_with(
921+
"get",
922+
revision_name,
923+
{"_url": {"name": revision_name}},
924+
None,
925+
)
926+
927+
@pytest.mark.asyncio
928+
async def test_get_skill_revision_async(self, async_skills_client):
929+
revision_name = "projects/test-project/locations/test-location/skills/test-skill/revisions/rev-1"
930+
mock_response = {
931+
"name": revision_name,
932+
"state": "ACTIVE",
933+
}
934+
935+
with mock.patch.object(
936+
async_skills_client._api_client, "async_request", autospec=True
937+
) as request_mock:
938+
request_mock.return_value = genai_types.HttpResponse(
939+
body=json.dumps(mock_response)
940+
)
941+
942+
revision = await async_skills_client.revisions.get(name=revision_name)
943+
944+
assert isinstance(revision, genai.types.SkillRevision)
945+
assert revision.name == revision_name
946+
assert revision.state == "ACTIVE"
947+
948+
request_mock.assert_called_once_with(
949+
"get",
950+
revision_name,
951+
{"_url": {"name": revision_name}},
952+
None,
953+
)
954+
955+
def test_list_skill_revisions(self, skills_client):
956+
skill_name = "projects/test-project/locations/test-location/skills/test-skill"
957+
mock_response = {
958+
"skillRevisions": [
959+
{
960+
"name": f"{skill_name}/revisions/rev-1",
961+
"state": "ACTIVE",
962+
}
963+
]
964+
}
965+
966+
with mock.patch.object(
967+
skills_client._api_client, "request", autospec=True
968+
) as request_mock:
969+
request_mock.return_value = genai_types.HttpResponse(
970+
body=json.dumps(mock_response)
971+
)
972+
973+
response = skills_client.revisions.list(name=skill_name)
974+
975+
assert isinstance(response, genai.types.ListSkillRevisionsResponse)
976+
assert len(response.skill_revisions) == 1
977+
assert response.skill_revisions[0].name == f"{skill_name}/revisions/rev-1"
978+
979+
request_mock.assert_called_once_with(
980+
"get",
981+
f"{skill_name}/revisions",
982+
{"_url": {"name": skill_name}},
983+
None,
984+
)
985+
986+
@pytest.mark.asyncio
987+
async def test_list_skill_revisions_async(self, async_skills_client):
988+
skill_name = "projects/test-project/locations/test-location/skills/test-skill"
989+
mock_response = {
990+
"skillRevisions": [
991+
{
992+
"name": f"{skill_name}/revisions/rev-1",
993+
"state": "ACTIVE",
994+
}
995+
]
996+
}
997+
998+
with mock.patch.object(
999+
async_skills_client._api_client, "async_request", autospec=True
1000+
) as request_mock:
1001+
request_mock.return_value = genai_types.HttpResponse(
1002+
body=json.dumps(mock_response)
1003+
)
1004+
1005+
response = await async_skills_client.revisions.list(name=skill_name)
1006+
1007+
assert isinstance(response, genai.types.ListSkillRevisionsResponse)
1008+
assert len(response.skill_revisions) == 1
1009+
assert response.skill_revisions[0].name == f"{skill_name}/revisions/rev-1"
1010+
1011+
request_mock.assert_called_once_with(
1012+
"get",
1013+
f"{skill_name}/revisions",
1014+
{"_url": {"name": skill_name}},
1015+
None,
1016+
)

0 commit comments

Comments
 (0)