c3bf08ac8d
K8s Workspace Integration Tests / k8s-workspace-tests (push) Waiting to run
Pre-commit / run (ubuntu-latest) (push) Waiting to run
Python Unittest Coverage / test (macos-15, 3.11) (push) Waiting to run
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Waiting to run
Python Unittest Coverage / test (windows-latest, 3.11) (push) Waiting to run
Web UI / check (push) Waiting to run
213 lines
7.3 KiB
Python
213 lines
7.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
# pylint: disable=protected-access
|
|
"""Test cases for LocalSkillLoader."""
|
|
import os
|
|
import tempfile
|
|
import shutil
|
|
import time
|
|
from unittest.async_case import IsolatedAsyncioTestCase
|
|
|
|
from agentscope.skill import LocalSkillLoader
|
|
|
|
|
|
class SkillLoaderTest(IsolatedAsyncioTestCase):
|
|
"""Test cases for LocalSkillLoader."""
|
|
|
|
async def asyncSetUp(self) -> None:
|
|
"""Set up test fixtures."""
|
|
# Create a temporary directory for testing
|
|
self.test_dir = tempfile.mkdtemp()
|
|
|
|
# Create test SKILL.md files
|
|
# 1. Root level skill
|
|
self.root_skill_content = """---
|
|
name: root_skill
|
|
description: A skill in the root directory
|
|
---
|
|
|
|
This is the root skill content.
|
|
"""
|
|
with open(
|
|
os.path.join(self.test_dir, "SKILL.md"),
|
|
"w",
|
|
encoding="utf-8",
|
|
) as f:
|
|
f.write(self.root_skill_content)
|
|
|
|
# 2. Subdirectory skill
|
|
self.subdir1 = os.path.join(self.test_dir, "subdir1")
|
|
os.makedirs(self.subdir1)
|
|
self.subdir1_skill_content = """---
|
|
name: subdir1_skill
|
|
description: A skill in subdirectory 1
|
|
---
|
|
|
|
This is the subdir1 skill content.
|
|
"""
|
|
with open(
|
|
os.path.join(self.subdir1, "SKILL.md"),
|
|
"w",
|
|
encoding="utf-8",
|
|
) as f:
|
|
f.write(self.subdir1_skill_content)
|
|
|
|
# 3. Nested subdirectory skill
|
|
self.subdir2 = os.path.join(self.test_dir, "subdir1", "subdir2")
|
|
os.makedirs(self.subdir2)
|
|
self.subdir2_skill_content = """---
|
|
name: subdir2_skill
|
|
description: A skill in nested subdirectory 2
|
|
---
|
|
|
|
This is the subdir2 skill content.
|
|
"""
|
|
with open(
|
|
os.path.join(self.subdir2, "SKILL.md"),
|
|
"w",
|
|
encoding="utf-8",
|
|
) as f:
|
|
f.write(self.subdir2_skill_content)
|
|
|
|
async def asyncTearDown(self) -> None:
|
|
"""Clean up test fixtures."""
|
|
# Remove the temporary directory
|
|
if os.path.exists(self.test_dir):
|
|
shutil.rmtree(self.test_dir)
|
|
|
|
async def test_nonexistent_skill(self) -> None:
|
|
"""Test loading from a directory without SKILL.md."""
|
|
# Create a directory without SKILL.md
|
|
empty_dir = tempfile.mkdtemp()
|
|
try:
|
|
loader = LocalSkillLoader(empty_dir, scan_subdir=False)
|
|
skills = await loader.list_skills()
|
|
self.assertEqual(len(skills), 0)
|
|
finally:
|
|
shutil.rmtree(empty_dir)
|
|
|
|
async def test_scan_subdir_flag_controls_subdirectory_scanning(
|
|
self,
|
|
) -> None:
|
|
"""Test that scan_subdir only controls subdirectory scanning.
|
|
|
|
The root directory is always scanned regardless of scan_subdir.
|
|
When scan_subdir=False, only root SKILL.md is loaded.
|
|
When scan_subdir=True, root + all subdirectory SKILL.md files
|
|
are loaded.
|
|
"""
|
|
loader_no_scan = LocalSkillLoader(self.test_dir, scan_subdir=False)
|
|
skills_no_scan = await loader_no_scan.list_skills()
|
|
|
|
loader_with_scan = LocalSkillLoader(self.test_dir, scan_subdir=True)
|
|
skills_with_scan = await loader_with_scan.list_skills()
|
|
|
|
# scan_subdir=False: only root skill
|
|
self.assertEqual(len(skills_no_scan), 1)
|
|
self.assertEqual(skills_no_scan[0].name, "root_skill")
|
|
|
|
# scan_subdir=True: root skill + both subdirectory skills
|
|
self.assertEqual(len(skills_with_scan), 3)
|
|
skill_names = {s.name for s in skills_with_scan}
|
|
self.assertIn("root_skill", skill_names)
|
|
self.assertIn("subdir1_skill", skill_names)
|
|
self.assertIn("subdir2_skill", skill_names)
|
|
|
|
async def test_load_skill_without_scan_subdir(self) -> None:
|
|
"""Test loading skill from root directory only (scan_subdir=False)."""
|
|
loader = LocalSkillLoader(self.test_dir, scan_subdir=False)
|
|
skills = await loader.list_skills()
|
|
|
|
# Should only load the root skill
|
|
self.assertEqual(len(skills), 1)
|
|
self.assertEqual(skills[0].name, "root_skill")
|
|
self.assertEqual(
|
|
skills[0].description,
|
|
"A skill in the root directory",
|
|
)
|
|
self.assertEqual(skills[0].dir, self.test_dir)
|
|
self.assertIn("This is the root skill content.", skills[0].markdown)
|
|
self.assertIsInstance(skills[0].updated_at, float)
|
|
self.assertGreater(skills[0].updated_at, 0)
|
|
|
|
async def test_load_skill_with_scan_subdir(self) -> None:
|
|
"""Test loading skills from subdirectories (scan_subdir=True)."""
|
|
loader = LocalSkillLoader(self.test_dir, scan_subdir=True)
|
|
skills = await loader.list_skills()
|
|
|
|
# Should load all three skills
|
|
self.assertEqual(len(skills), 3)
|
|
|
|
# Check skill names
|
|
skill_names = {skill.name for skill in skills}
|
|
self.assertEqual(
|
|
skill_names,
|
|
{"root_skill", "subdir1_skill", "subdir2_skill"},
|
|
)
|
|
|
|
# Verify each skill has correct attributes
|
|
for skill in skills:
|
|
self.assertIsInstance(skill.name, str)
|
|
self.assertIsInstance(skill.description, str)
|
|
self.assertIsInstance(skill.dir, str)
|
|
self.assertIsInstance(skill.markdown, str)
|
|
self.assertIsInstance(skill.updated_at, float)
|
|
self.assertGreater(skill.updated_at, 0)
|
|
|
|
async def test_cache_mechanism(self) -> None:
|
|
"""Test that cache is used when file is not modified."""
|
|
loader = LocalSkillLoader(self.test_dir, scan_subdir=False)
|
|
|
|
# First load - should read from file
|
|
skills_first = await loader.list_skills()
|
|
self.assertEqual(len(skills_first), 1)
|
|
first_skill = skills_first[0]
|
|
|
|
# Verify cache is populated
|
|
self.assertIn(self.test_dir, loader._cache)
|
|
cached_skill = loader._cache[self.test_dir]
|
|
self.assertEqual(cached_skill.name, first_skill.name)
|
|
self.assertEqual(cached_skill.updated_at, first_skill.updated_at)
|
|
|
|
# Second load - should use cache (file not modified)
|
|
skills_second = await loader.list_skills()
|
|
self.assertEqual(len(skills_second), 1)
|
|
second_skill = skills_second[0]
|
|
|
|
# Should return the same cached object
|
|
self.assertIs(second_skill, cached_skill)
|
|
self.assertEqual(second_skill.updated_at, first_skill.updated_at)
|
|
|
|
# Modify the file
|
|
time.sleep(0.01) # Ensure different mtime
|
|
modified_content = """---
|
|
name: modified_root_skill
|
|
description: Modified skill description
|
|
---
|
|
|
|
This is the modified content.
|
|
"""
|
|
with open(
|
|
os.path.join(self.test_dir, "SKILL.md"),
|
|
"w",
|
|
encoding="utf-8",
|
|
) as f:
|
|
f.write(modified_content)
|
|
|
|
# Third load - should detect change and reload
|
|
skills_third = await loader.list_skills()
|
|
self.assertEqual(len(skills_third), 1)
|
|
third_skill = skills_third[0]
|
|
|
|
# Should have new content
|
|
self.assertEqual(third_skill.name, "modified_root_skill")
|
|
self.assertEqual(third_skill.description, "Modified skill description")
|
|
self.assertIn("This is the modified content.", third_skill.markdown)
|
|
self.assertNotEqual(third_skill.updated_at, first_skill.updated_at)
|
|
|
|
# Cache should be updated
|
|
self.assertIn(self.test_dir, loader._cache)
|
|
self.assertEqual(
|
|
loader._cache[self.test_dir].name,
|
|
"modified_root_skill",
|
|
)
|