mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-24 08:55:59 +00:00
refactor(skills): Unified skill storage capability (#2613)
This commit is contained in:
@@ -8,7 +8,7 @@ from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.gateway.routers import skills as skills_router
|
||||
from deerflow.skills.manager import get_skill_history_file
|
||||
from deerflow.skills.storage import get_or_new_skill_storage
|
||||
from deerflow.skills.types import Skill
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ def test_install_skill_archive_runs_security_scan(monkeypatch, tmp_path):
|
||||
scan_calls = []
|
||||
refresh_calls = []
|
||||
|
||||
async def _scan(content, *, executable, location):
|
||||
async def _scan(content, *, executable, location, app_config=None):
|
||||
from deerflow.skills.security_scanner import ScanResult
|
||||
|
||||
scan_calls.append({"content": content, "executable": executable, "location": location})
|
||||
@@ -67,13 +67,21 @@ def test_install_skill_archive_runs_security_scan(monkeypatch, tmp_path):
|
||||
async def _refresh():
|
||||
refresh_calls.append("refresh")
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from deerflow.skills.storage.local_skill_storage import LocalSkillStorage
|
||||
|
||||
storage = LocalSkillStorage(host_path=str(skills_root))
|
||||
config = SimpleNamespace(
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills", use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage"),
|
||||
skill_evolution=SimpleNamespace(enabled=True, moderation_model_name=None),
|
||||
)
|
||||
monkeypatch.setattr(skills_router, "resolve_thread_virtual_path", lambda thread_id, path: archive)
|
||||
monkeypatch.setattr("deerflow.skills.installer.get_skills_root_path", lambda: skills_root)
|
||||
monkeypatch.setattr(skills_router, "get_or_new_skill_storage", lambda **kw: storage)
|
||||
monkeypatch.setattr("deerflow.skills.installer.scan_skill_content", _scan)
|
||||
monkeypatch.setattr(skills_router, "refresh_skills_system_prompt_cache_async", _refresh)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(skills_router.router)
|
||||
app = _make_test_app(config)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/api/skills/install", json={"thread_id": "thread-1", "path": "mnt/user-data/outputs/archive-skill.skill"})
|
||||
@@ -105,13 +113,21 @@ def test_install_skill_archive_security_scan_block_returns_400(monkeypatch, tmp_
|
||||
async def _refresh():
|
||||
refresh_calls.append("refresh")
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
from deerflow.skills.storage.local_skill_storage import LocalSkillStorage
|
||||
|
||||
storage = LocalSkillStorage(host_path=str(skills_root))
|
||||
config = SimpleNamespace(
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills", use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage"),
|
||||
skill_evolution=SimpleNamespace(enabled=True, moderation_model_name=None),
|
||||
)
|
||||
monkeypatch.setattr(skills_router, "resolve_thread_virtual_path", lambda thread_id, path: archive)
|
||||
monkeypatch.setattr("deerflow.skills.installer.get_skills_root_path", lambda: skills_root)
|
||||
monkeypatch.setattr(skills_router, "get_or_new_skill_storage", lambda **kw: storage)
|
||||
monkeypatch.setattr("deerflow.skills.installer.scan_skill_content", _scan)
|
||||
monkeypatch.setattr(skills_router, "refresh_skills_system_prompt_cache_async", _refresh)
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(skills_router.router)
|
||||
app = _make_test_app(config)
|
||||
|
||||
with TestClient(app) as client:
|
||||
response = client.post("/api/skills/install", json={"thread_id": "thread-1", "path": "mnt/user-data/outputs/blocked-skill.skill"})
|
||||
@@ -128,11 +144,10 @@ def test_custom_skills_router_lifecycle(monkeypatch, tmp_path):
|
||||
custom_dir.mkdir(parents=True, exist_ok=True)
|
||||
(custom_dir / "SKILL.md").write_text(_skill_content("demo-skill"), encoding="utf-8")
|
||||
config = SimpleNamespace(
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills"),
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills", use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage"),
|
||||
skill_evolution=SimpleNamespace(enabled=True, moderation_model_name=None),
|
||||
)
|
||||
monkeypatch.setattr("deerflow.config.get_app_config", lambda: config)
|
||||
monkeypatch.setattr("deerflow.skills.manager.get_app_config", lambda: config)
|
||||
monkeypatch.setattr("app.gateway.routers.skills.scan_skill_content", lambda *args, **kwargs: _async_scan("allow", "ok"))
|
||||
refresh_calls = []
|
||||
|
||||
@@ -177,12 +192,13 @@ def test_custom_skill_rollback_blocked_by_scanner(monkeypatch, tmp_path):
|
||||
edited_content = _skill_content("demo-skill", "Edited skill")
|
||||
(custom_dir / "SKILL.md").write_text(edited_content, encoding="utf-8")
|
||||
config = SimpleNamespace(
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills"),
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills", use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage"),
|
||||
skill_evolution=SimpleNamespace(enabled=True, moderation_model_name=None),
|
||||
)
|
||||
monkeypatch.setattr("deerflow.config.get_app_config", lambda: config)
|
||||
monkeypatch.setattr("deerflow.skills.manager.get_app_config", lambda: config)
|
||||
get_skill_history_file("demo-skill", app_config=config).write_text(
|
||||
history_file = get_or_new_skill_storage(app_config=config).get_skill_history_file("demo-skill")
|
||||
history_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
history_file.write_text(
|
||||
'{"action":"human_edit","prev_content":' + json.dumps(original_content) + ',"new_content":' + json.dumps(edited_content) + "}\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
@@ -218,11 +234,10 @@ def test_custom_skill_delete_preserves_history_and_allows_restore(monkeypatch, t
|
||||
original_content = _skill_content("demo-skill")
|
||||
(custom_dir / "SKILL.md").write_text(original_content, encoding="utf-8")
|
||||
config = SimpleNamespace(
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills"),
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills", use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage"),
|
||||
skill_evolution=SimpleNamespace(enabled=True, moderation_model_name=None),
|
||||
)
|
||||
monkeypatch.setattr("deerflow.config.get_app_config", lambda: config)
|
||||
monkeypatch.setattr("deerflow.skills.manager.get_app_config", lambda: config)
|
||||
monkeypatch.setattr("app.gateway.routers.skills.scan_skill_content", lambda *args, **kwargs: _async_scan("allow", "ok"))
|
||||
refresh_calls = []
|
||||
|
||||
@@ -255,11 +270,10 @@ def test_custom_skill_delete_continues_when_history_write_is_readonly(monkeypatc
|
||||
custom_dir.mkdir(parents=True, exist_ok=True)
|
||||
(custom_dir / "SKILL.md").write_text(_skill_content("demo-skill"), encoding="utf-8")
|
||||
config = SimpleNamespace(
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills"),
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills", use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage"),
|
||||
skill_evolution=SimpleNamespace(enabled=True, moderation_model_name=None),
|
||||
)
|
||||
monkeypatch.setattr("deerflow.config.get_app_config", lambda: config)
|
||||
monkeypatch.setattr("deerflow.skills.manager.get_app_config", lambda: config)
|
||||
refresh_calls = []
|
||||
|
||||
async def _refresh():
|
||||
@@ -268,7 +282,7 @@ def test_custom_skill_delete_continues_when_history_write_is_readonly(monkeypatc
|
||||
def _readonly_history(*args, **kwargs):
|
||||
raise OSError(errno.EROFS, "Read-only file system", str(skills_root / "custom" / ".history"))
|
||||
|
||||
monkeypatch.setattr("app.gateway.routers.skills.append_history", _readonly_history)
|
||||
monkeypatch.setattr("deerflow.skills.storage.local_skill_storage.LocalSkillStorage.append_history", _readonly_history)
|
||||
monkeypatch.setattr("app.gateway.routers.skills.refresh_skills_system_prompt_cache_async", _refresh)
|
||||
|
||||
app = _make_test_app(config)
|
||||
@@ -288,11 +302,10 @@ def test_custom_skill_delete_fails_when_skill_dir_removal_fails(monkeypatch, tmp
|
||||
custom_dir.mkdir(parents=True, exist_ok=True)
|
||||
(custom_dir / "SKILL.md").write_text(_skill_content("demo-skill"), encoding="utf-8")
|
||||
config = SimpleNamespace(
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills"),
|
||||
skills=SimpleNamespace(get_skills_path=lambda: skills_root, container_path="/mnt/skills", use="deerflow.skills.storage.local_skill_storage:LocalSkillStorage"),
|
||||
skill_evolution=SimpleNamespace(enabled=True, moderation_model_name=None),
|
||||
)
|
||||
monkeypatch.setattr("deerflow.config.get_app_config", lambda: config)
|
||||
monkeypatch.setattr("deerflow.skills.manager.get_app_config", lambda: config)
|
||||
refresh_calls = []
|
||||
|
||||
async def _refresh():
|
||||
@@ -301,7 +314,7 @@ def test_custom_skill_delete_fails_when_skill_dir_removal_fails(monkeypatch, tmp
|
||||
def _fail_rmtree(*args, **kwargs):
|
||||
raise PermissionError(errno.EACCES, "Permission denied", str(custom_dir))
|
||||
|
||||
monkeypatch.setattr("app.gateway.routers.skills.shutil.rmtree", _fail_rmtree)
|
||||
monkeypatch.setattr("deerflow.skills.storage.local_skill_storage.shutil.rmtree", _fail_rmtree)
|
||||
monkeypatch.setattr("app.gateway.routers.skills.refresh_skills_system_prompt_cache_async", _refresh)
|
||||
|
||||
app = _make_test_app(config)
|
||||
@@ -320,7 +333,7 @@ def test_update_skill_refreshes_prompt_cache_before_return(monkeypatch, tmp_path
|
||||
enabled_state = {"value": True}
|
||||
refresh_calls = []
|
||||
|
||||
def _load_skills(*, enabled_only: bool, app_config=None):
|
||||
def _load_skills(*, enabled_only: bool):
|
||||
skill = _make_skill("demo-skill", enabled=enabled_state["value"])
|
||||
if enabled_only and not skill.enabled:
|
||||
return []
|
||||
@@ -330,7 +343,8 @@ def test_update_skill_refreshes_prompt_cache_before_return(monkeypatch, tmp_path
|
||||
refresh_calls.append("refresh")
|
||||
enabled_state["value"] = False
|
||||
|
||||
monkeypatch.setattr("app.gateway.routers.skills.load_skills", _load_skills)
|
||||
mock_storage = SimpleNamespace(load_skills=_load_skills)
|
||||
monkeypatch.setattr("app.gateway.routers.skills.get_or_new_skill_storage", lambda **kwargs: mock_storage)
|
||||
monkeypatch.setattr("app.gateway.routers.skills.get_extensions_config", lambda: SimpleNamespace(mcp_servers={}, skills={}))
|
||||
monkeypatch.setattr("app.gateway.routers.skills.reload_extensions_config", lambda: None)
|
||||
monkeypatch.setattr(skills_router.ExtensionsConfig, "resolve_config_path", staticmethod(lambda: config_path))
|
||||
|
||||
Reference in New Issue
Block a user