Refactor conversation persistence into workspace-scoped storage with host workspace legacy migration, current workspace filtering, and active host workspace state protection. Add proactive recent-conversation prompt injection with personal-space controls, configurable item count, empty-conversation filtering, and frozen per-conversation prompt metadata to keep request caches stable. Introduce conversation_search and conversation_review tools for current-workspace history: multi-keyword search, list mode, current conversation exclusion, message/tool counts, formatted model output, read/save review modes, long-review fallback, and .agents/review storage. Move internal workspace artifacts under .agents, including uploads, skills, compact results, and reviews; update prompt file-tree visibility, compression output, upload paths, and hidden versioning exclusions. Add create_skill workflow and a Skills tool category, move read_skill into Skills, validate/归档 skill folders, support host global skills and Docker/API private skills, and sync private skills into .agents/skills and skill prompts. Update frontend tool icons, statuses, and expanded renderers for conversation recall and skills workflows; add workspace conversation and skills-manager tests.
98 lines
3.9 KiB
Python
98 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from modules.skills_manager import (
|
|
archive_skill_directory,
|
|
get_skills_catalog,
|
|
infer_private_skills_dir,
|
|
sync_workspace_skills,
|
|
validate_skill_directory,
|
|
)
|
|
|
|
|
|
class SkillsManagerTest(unittest.TestCase):
|
|
def test_validate_skill_directory_requires_skill_md_and_frontmatter_fields(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
skill_dir = root / "sample-skill"
|
|
skill_dir.mkdir()
|
|
|
|
missing_file = validate_skill_directory(skill_dir)
|
|
self.assertFalse(missing_file.get("success"))
|
|
self.assertEqual(missing_file.get("error"), "缺少 SKILL.md")
|
|
|
|
(skill_dir / "SKILL.md").write_text("---\nname: Sample\n---\n", encoding="utf-8")
|
|
missing_description = validate_skill_directory(skill_dir)
|
|
self.assertFalse(missing_description.get("success"))
|
|
self.assertEqual(missing_description.get("error"), "缺少 description:")
|
|
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"---\nname: Sample\ndescription: Demo skill\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
valid = validate_skill_directory(skill_dir)
|
|
self.assertTrue(valid.get("success"))
|
|
self.assertEqual(valid.get("skill_name"), "sample-skill")
|
|
|
|
def test_archive_skill_directory_moves_without_overwriting(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
skill_dir = root / "sample-skill"
|
|
target_root = root / "agentskills"
|
|
skill_dir.mkdir()
|
|
(skill_dir / "SKILL.md").write_text(
|
|
"---\nname: Sample\ndescription: Demo skill\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
archived = archive_skill_directory(skill_dir, target_root)
|
|
self.assertTrue(archived.get("success"))
|
|
self.assertFalse(skill_dir.exists())
|
|
self.assertTrue((target_root / "sample-skill" / "SKILL.md").exists())
|
|
|
|
duplicate_src = root / "sample-skill"
|
|
duplicate_src.mkdir()
|
|
(duplicate_src / "SKILL.md").write_text(
|
|
"---\nname: Sample\ndescription: Demo skill\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
duplicate = archive_skill_directory(duplicate_src, target_root)
|
|
self.assertFalse(duplicate.get("success"))
|
|
self.assertEqual(duplicate.get("error"), "目标 skill 已存在")
|
|
|
|
def test_private_skills_are_cataloged_and_synced(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
global_root = root / "global"
|
|
private_root = root / "users" / "jojo" / "agentskills"
|
|
data_dir = root / "users" / "jojo" / "data"
|
|
project = root / "project"
|
|
private_skill = private_root / "private-skill"
|
|
private_skill.mkdir(parents=True)
|
|
data_dir.mkdir(parents=True)
|
|
project.mkdir(parents=True)
|
|
(private_skill / "SKILL.md").write_text(
|
|
"---\nname: Private Skill\ndescription: Private demo\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
self.assertEqual(infer_private_skills_dir(data_dir), private_root.resolve())
|
|
catalog = get_skills_catalog(base_dir=str(global_root), private_dir=private_root)
|
|
self.assertEqual([item["id"] for item in catalog], ["private-skill"])
|
|
|
|
synced = sync_workspace_skills(
|
|
project,
|
|
enabled_skills=["private-skill"],
|
|
base_dir=str(global_root),
|
|
private_dir=private_root,
|
|
)
|
|
self.assertTrue(synced.get("success"))
|
|
self.assertTrue((project / ".agents" / "skills" / "private-skill" / "SKILL.md").exists())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|