* fix(harness): restore legacy skills path fallback (#2694) * fix(format): make format * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,13 @@ from pydantic import BaseModel, Field
|
|||||||
from deerflow.config.runtime_paths import project_root, resolve_path
|
from deerflow.config.runtime_paths import project_root, resolve_path
|
||||||
|
|
||||||
|
|
||||||
|
def _legacy_skills_candidates() -> tuple[Path, ...]:
|
||||||
|
"""Return source-tree skills locations for monorepo compatibility."""
|
||||||
|
backend_dir = Path(__file__).resolve().parents[4]
|
||||||
|
repo_root = backend_dir.parent
|
||||||
|
return (repo_root / "skills",)
|
||||||
|
|
||||||
|
|
||||||
class SkillsConfig(BaseModel):
|
class SkillsConfig(BaseModel):
|
||||||
"""Configuration for skills system"""
|
"""Configuration for skills system"""
|
||||||
|
|
||||||
@@ -15,7 +22,7 @@ class SkillsConfig(BaseModel):
|
|||||||
)
|
)
|
||||||
path: str | None = Field(
|
path: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Path to skills directory. If not specified, defaults to skills under the caller project root.",
|
description=("Path to skills directory. If not specified, defaults to `skills` under the caller project root, falling back to the legacy repo-root location for monorepo compatibility."),
|
||||||
)
|
)
|
||||||
container_path: str = Field(
|
container_path: str = Field(
|
||||||
default="/mnt/skills",
|
default="/mnt/skills",
|
||||||
@@ -26,15 +33,30 @@ class SkillsConfig(BaseModel):
|
|||||||
"""
|
"""
|
||||||
Get the resolved skills directory path.
|
Get the resolved skills directory path.
|
||||||
|
|
||||||
Returns:
|
Resolution order:
|
||||||
Path to the skills directory
|
1. Explicit ``path`` field
|
||||||
|
2. ``DEER_FLOW_SKILLS_PATH`` environment variable
|
||||||
|
3. ``skills`` under the caller project root (``project_root()``)
|
||||||
|
4. Legacy repo-root candidates for monorepo compatibility (``_legacy_skills_candidates``)
|
||||||
|
|
||||||
|
When none of (3) or (4) exist on disk, the project-root default is returned so callers
|
||||||
|
can still surface a stable "no skills" location without raising.
|
||||||
"""
|
"""
|
||||||
if self.path:
|
if self.path:
|
||||||
# Use configured path (can be absolute or relative to project root)
|
# Use configured path (can be absolute or relative to project root)
|
||||||
return resolve_path(self.path)
|
return resolve_path(self.path)
|
||||||
if env_path := os.getenv("DEER_FLOW_SKILLS_PATH"):
|
if env_path := os.getenv("DEER_FLOW_SKILLS_PATH"):
|
||||||
return resolve_path(env_path)
|
return resolve_path(env_path)
|
||||||
return project_root() / "skills"
|
|
||||||
|
project_default = project_root() / "skills"
|
||||||
|
if project_default.is_dir():
|
||||||
|
return project_default
|
||||||
|
|
||||||
|
for candidate in _legacy_skills_candidates():
|
||||||
|
if candidate.is_dir():
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
return project_default
|
||||||
|
|
||||||
def get_skill_container_path(self, skill_name: str, category: str = "public") -> str:
|
def get_skill_container_path(self, skill_name: str, category: str = "public") -> str:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import yaml
|
|||||||
|
|
||||||
from deerflow.config import app_config as app_config_module
|
from deerflow.config import app_config as app_config_module
|
||||||
from deerflow.config import extensions_config as extensions_config_module
|
from deerflow.config import extensions_config as extensions_config_module
|
||||||
|
from deerflow.config import skills_config as skills_config_module
|
||||||
from deerflow.config.app_config import AppConfig
|
from deerflow.config.app_config import AppConfig
|
||||||
from deerflow.config.extensions_config import ExtensionsConfig
|
from deerflow.config.extensions_config import ExtensionsConfig
|
||||||
from deerflow.config.paths import Paths
|
from deerflow.config.paths import Paths
|
||||||
@@ -35,6 +36,7 @@ def test_default_runtime_paths_resolve_from_current_project(tmp_path: Path, monk
|
|||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
(tmp_path / "extensions_config.json").write_text('{"mcpServers": {}, "skills": {}}', encoding="utf-8")
|
(tmp_path / "extensions_config.json").write_text('{"mcpServers": {}, "skills": {}}', encoding="utf-8")
|
||||||
|
(tmp_path / "skills").mkdir()
|
||||||
|
|
||||||
assert AppConfig.resolve_config_path() == tmp_path / "config.yaml"
|
assert AppConfig.resolve_config_path() == tmp_path / "config.yaml"
|
||||||
assert ExtensionsConfig.resolve_config_path() == tmp_path / "extensions_config.json"
|
assert ExtensionsConfig.resolve_config_path() == tmp_path / "extensions_config.json"
|
||||||
@@ -121,6 +123,40 @@ def test_app_config_falls_back_to_legacy_when_project_root_lacks_config(tmp_path
|
|||||||
assert AppConfig.resolve_config_path() == legacy_backend_config
|
assert AppConfig.resolve_config_path() == legacy_backend_config
|
||||||
|
|
||||||
|
|
||||||
|
def test_skills_config_falls_back_to_legacy_when_project_root_lacks_skills(tmp_path: Path, monkeypatch):
|
||||||
|
"""When DEER_FLOW_PROJECT_ROOT is unset and cwd has no `skills/`, the legacy
|
||||||
|
repo-root candidate must be used so monorepo runs (cwd=backend/) keep finding
|
||||||
|
`<repo>/skills` instead of `<repo>/backend/skills` (regression test for #2694)."""
|
||||||
|
_clear_path_env(monkeypatch)
|
||||||
|
cwd = tmp_path / "cwd"
|
||||||
|
cwd.mkdir()
|
||||||
|
monkeypatch.chdir(cwd)
|
||||||
|
|
||||||
|
legacy_skills = tmp_path / "legacy-repo" / "skills"
|
||||||
|
legacy_skills.mkdir(parents=True)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
skills_config_module,
|
||||||
|
"_legacy_skills_candidates",
|
||||||
|
lambda: (legacy_skills,),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert SkillsConfig().get_skills_path() == legacy_skills
|
||||||
|
|
||||||
|
|
||||||
|
def test_skills_config_returns_project_default_when_neither_exists(tmp_path: Path, monkeypatch):
|
||||||
|
"""When nothing exists, fall back to the project-root default path so callers
|
||||||
|
surface a stable empty location instead of silently picking a stale legacy dir."""
|
||||||
|
_clear_path_env(monkeypatch)
|
||||||
|
cwd = tmp_path / "cwd"
|
||||||
|
cwd.mkdir()
|
||||||
|
monkeypatch.chdir(cwd)
|
||||||
|
|
||||||
|
monkeypatch.setattr(skills_config_module, "_legacy_skills_candidates", lambda: ())
|
||||||
|
|
||||||
|
assert SkillsConfig().get_skills_path() == cwd / "skills"
|
||||||
|
|
||||||
|
|
||||||
def test_extensions_config_falls_back_to_legacy_when_project_root_lacks_file(tmp_path: Path, monkeypatch):
|
def test_extensions_config_falls_back_to_legacy_when_project_root_lacks_file(tmp_path: Path, monkeypatch):
|
||||||
"""ExtensionsConfig should hit the legacy backend/repo-root locations when
|
"""ExtensionsConfig should hit the legacy backend/repo-root locations when
|
||||||
the caller project root has no extensions_config.json/mcp_config.json."""
|
the caller project root has no extensions_config.json/mcp_config.json."""
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ def test_get_skills_root_path_points_to_current_project_skills(tmp_path: Path, m
|
|||||||
monkeypatch.delenv("DEER_FLOW_SKILLS_PATH", raising=False)
|
monkeypatch.delenv("DEER_FLOW_SKILLS_PATH", raising=False)
|
||||||
monkeypatch.delenv("DEER_FLOW_PROJECT_ROOT", raising=False)
|
monkeypatch.delenv("DEER_FLOW_PROJECT_ROOT", raising=False)
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
|
(tmp_path / "skills").mkdir()
|
||||||
|
|
||||||
app_config = SimpleNamespace(skills=SkillsConfig())
|
app_config = SimpleNamespace(skills=SkillsConfig())
|
||||||
path = get_or_new_skill_storage(app_config=app_config).get_skills_root_path()
|
path = get_or_new_skill_storage(app_config=app_config).get_skills_root_path()
|
||||||
|
|||||||
Reference in New Issue
Block a user