mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-27 05:30:39 +00:00
* fix(skills): keep custom-skill edit checks off the event loop update_custom_skill built the user-scoped storage, ran the editability probes, validated the frontmatter through a temporary directory and read the content being replaced on the Gateway loop; only the write and history append had been offloaded. Run the pre-scan checks in one worker and the replaced-content read in another, matching rollback_custom_skill, and build delete_custom_skill's storage off the loop as well. * fix(skills): build skill storage off the loop in install and skill_manage The same sweep that moved the custom-skill edit checks off the loop finds two more storage lookups on it: the archive-install helper behind both install routes, and the agent's skill_manage tool, whose lookup resolves and stats config.yaml on every call. Offload both, and pin that the caller's user context still reaches the storage lookup now that the edit route resolves it inside a worker. * docs(changelog): reference #6332 for the skill storage off-loop fix --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
65 lines
2.5 KiB
Python
65 lines
2.5 KiB
Python
"""Regression anchor: the agent's ``skill_manage`` tool must keep its storage lookup off the loop.
|
|
|
|
``_skill_manage_impl`` offloads every per-action filesystem step through
|
|
``asyncio.to_thread``. The user-scoped storage lookup in front of them resolves
|
|
the app config (``get_app_config`` stats ``config.yaml`` on every call) and, on
|
|
a cold cache, the project root, so it runs in a worker as well -- the same rule
|
|
the Gateway's custom-skill edit route applies.
|
|
|
|
Only the LLM security scan is stubbed; the app config is pinned so the lookup
|
|
needs no ``config.yaml`` on disk, and the edit runs against the real per-user
|
|
storage under ``DEER_FLOW_HOME``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
import deerflow.tools.skill_manage_tool as skill_manage_module
|
|
from deerflow.config.app_config import AppConfig
|
|
from deerflow.config.paths import get_paths
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
_SKILL_NAME = "loop-manage-skill"
|
|
_SKILL_MD = f"---\nname: {_SKILL_NAME}\ndescription: Anchor fixture skill.\n---\n\n# {_SKILL_NAME}\n"
|
|
_RUNTIME = SimpleNamespace(
|
|
context={"thread_id": "thread-1", "user_id": "default"},
|
|
config={"configurable": {"thread_id": "thread-1", "user_id": "default"}},
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_paths(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path))
|
|
monkeypatch.setattr("deerflow.config.paths._paths", None)
|
|
|
|
|
|
def _install_skill() -> Path:
|
|
skill_dir = get_paths().user_custom_skills_dir("default") / _SKILL_NAME
|
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
skill_file = skill_dir / "SKILL.md"
|
|
skill_file.write_text(_SKILL_MD, encoding="utf-8")
|
|
return skill_file
|
|
|
|
|
|
async def test_skill_manage_edit_does_not_block_event_loop(monkeypatch) -> None:
|
|
skill_file = await asyncio.to_thread(_install_skill)
|
|
config = AppConfig.model_validate({"sandbox": {"use": "test"}})
|
|
monkeypatch.setattr("deerflow.config.get_app_config", lambda: config)
|
|
|
|
async def _allow_scan(*args, **kwargs):
|
|
return SimpleNamespace(decision="allow", reason="anchor stub")
|
|
|
|
monkeypatch.setattr(skill_manage_module, "scan_skill_content", _allow_scan)
|
|
edited = _SKILL_MD + "\nEdited.\n"
|
|
|
|
result = await skill_manage_module.skill_manage_tool.coroutine(_RUNTIME, "edit", _SKILL_NAME, edited)
|
|
|
|
assert result == f"Updated custom skill '{_SKILL_NAME}'."
|
|
assert await asyncio.to_thread(skill_file.read_text, encoding="utf-8") == edited
|