Implement workspace-scoped conversation storage with legacy migration based on host workspace metadata, active host workspace state protection, and frontend/backend conversation list scoping. Add proactive recent-conversation prompt support with a configurable personal-space limit, frozen per conversation metadata, empty-conversation filtering, and a dedicated prompt template. Add conversation_search and conversation_review tools with readable formatted results, workspace-only access, current-conversation exclusion, multi-keyword search, list mode, message/tool counts, read/save review modes, long-review fallback to .agents/review, and frontend detail renderers. Move internal workspace artifacts under .agents, update upload/skills/compact/review paths, show .agents internal directories in prompt file trees, and adjust versioning ignores. Add create_skill and a Skills tool category, move read_skill into that category, validate and archive skill folders, support host global skills and Docker/API private skills, and sync private skills into .agents/skills and prompts. Update memory stamping with conversation/time source metadata and include frontend icon/status/display updates plus tests for workspace conversation storage and skills management.
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()
|