Add directory-based project skills
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from src.bundled_skills import (
|
||||
find_bundled_skill,
|
||||
format_skills_for_system_prompt,
|
||||
get_bundled_skills,
|
||||
load_directory_skills,
|
||||
load_project_skills,
|
||||
)
|
||||
from src.agent_runtime import LocalCodingAgent
|
||||
from src.agent_types import AgentRuntimeConfig, ModelConfig
|
||||
|
||||
|
||||
class BundledSkillsTests(unittest.TestCase):
|
||||
def test_directory_skills_are_loaded_with_metadata(self) -> None:
|
||||
skills = {skill.name: skill for skill in get_bundled_skills()}
|
||||
self.assertIn('verify', skills)
|
||||
self.assertEqual(skills['verify'].source, 'directory')
|
||||
self.assertIn('bash', skills['verify'].allowed_tools)
|
||||
|
||||
def test_directory_skill_aliases_are_resolved(self) -> None:
|
||||
skill = find_bundled_skill('config-help')
|
||||
self.assertIsNotNone(skill)
|
||||
assert skill is not None
|
||||
self.assertEqual(skill.name, 'update-config')
|
||||
self.assertEqual(skill.source, 'directory')
|
||||
|
||||
def test_directory_skill_prompt_appends_invocation_args(self) -> None:
|
||||
skill = find_bundled_skill('verify')
|
||||
self.assertIsNotNone(skill)
|
||||
assert skill is not None
|
||||
prompt = skill.get_prompt(None, 'focus: tests') # type: ignore[arg-type]
|
||||
self.assertIn('Verify that the recent code changes work correctly.', prompt)
|
||||
self.assertIn('## Invocation Arguments', prompt)
|
||||
self.assertIn('focus: tests', prompt)
|
||||
|
||||
def test_custom_directory_skill_loader(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
root = Path(tmp_dir)
|
||||
skill_dir = root / 'sample'
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / 'SKILL.md').write_text(
|
||||
(
|
||||
'---\n'
|
||||
'name: sample\n'
|
||||
'description: Sample directory skill.\n'
|
||||
'aliases: s1, s2\n'
|
||||
'allowed_tools: read_file, write_file\n'
|
||||
'---\n'
|
||||
'Use sample instructions.\n'
|
||||
),
|
||||
encoding='utf-8',
|
||||
)
|
||||
skills = load_directory_skills(root)
|
||||
self.assertEqual(len(skills), 1)
|
||||
self.assertEqual(skills[0].name, 'sample')
|
||||
self.assertEqual(skills[0].aliases, ('s1', 's2'))
|
||||
self.assertEqual(skills[0].allowed_tools, ('read_file', 'write_file'))
|
||||
|
||||
def test_skills_prompt_includes_directory_skill(self) -> None:
|
||||
rendered = format_skills_for_system_prompt()
|
||||
self.assertIn('verify', rendered)
|
||||
self.assertIn('update-config', rendered)
|
||||
|
||||
def test_project_skills_are_loaded_from_workspace_skills_directory(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
skill_dir = workspace / 'skills' / 'project-skill'
|
||||
skill_dir.mkdir(parents=True)
|
||||
(skill_dir / 'SKILL.md').write_text(
|
||||
(
|
||||
'---\n'
|
||||
'name: project-skill\n'
|
||||
'description: Project-maintained skill.\n'
|
||||
'aliases: local-skill\n'
|
||||
'allowed_tools: read_file\n'
|
||||
'---\n'
|
||||
'Project skill body.\n'
|
||||
),
|
||||
encoding='utf-8',
|
||||
)
|
||||
skills = get_bundled_skills(workspace)
|
||||
resolved = find_bundled_skill('local-skill', cwd=workspace)
|
||||
by_name = {skill.name: skill for skill in skills}
|
||||
self.assertIn('project-skill', by_name)
|
||||
self.assertEqual(by_name['project-skill'].source, 'project')
|
||||
self.assertIsNotNone(resolved)
|
||||
assert resolved is not None
|
||||
self.assertEqual(resolved.name, 'project-skill')
|
||||
|
||||
def test_project_skills_override_bundled_directory_skills(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
skill_dir = workspace / 'skills' / 'verify'
|
||||
skill_dir.mkdir(parents=True)
|
||||
(skill_dir / 'SKILL.md').write_text(
|
||||
(
|
||||
'---\n'
|
||||
'name: verify\n'
|
||||
'description: Project override for verify.\n'
|
||||
'---\n'
|
||||
'Project verify body.\n'
|
||||
),
|
||||
encoding='utf-8',
|
||||
)
|
||||
skill = find_bundled_skill('verify', cwd=workspace)
|
||||
self.assertIsNotNone(skill)
|
||||
assert skill is not None
|
||||
self.assertEqual(skill.source, 'project')
|
||||
self.assertIn('Project verify body.', skill.get_prompt(None, '')) # type: ignore[arg-type]
|
||||
|
||||
def test_load_project_skills_returns_empty_without_skills_directory(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
self.assertEqual(load_project_skills(Path(tmp_dir)), ())
|
||||
|
||||
def test_agent_skill_tool_executes_project_skill_from_cwd(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
workspace = Path(tmp_dir)
|
||||
skill_dir = workspace / 'skills' / 'project-run'
|
||||
skill_dir.mkdir(parents=True)
|
||||
(skill_dir / 'SKILL.md').write_text(
|
||||
(
|
||||
'---\n'
|
||||
'name: project-run\n'
|
||||
'description: Project runnable skill.\n'
|
||||
'---\n'
|
||||
'Run project skill body.\n'
|
||||
),
|
||||
encoding='utf-8',
|
||||
)
|
||||
agent = LocalCodingAgent(
|
||||
model_config=ModelConfig(model='test'),
|
||||
runtime_config=AgentRuntimeConfig(cwd=workspace),
|
||||
)
|
||||
result = agent._execute_skill({'skill': 'project-run', 'args': 'demo'})
|
||||
self.assertTrue(result.ok)
|
||||
self.assertIn('Run project skill body.', result.content)
|
||||
self.assertEqual(result.metadata.get('source'), 'project')
|
||||
self.assertIn('SKILL.md', str(result.metadata.get('skill_path')))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user