Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:39:27 +08:00

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",
)