deer-flow/backend/tests/test_runtime_paths.py
Nan Gao c09c334544
fix(harness): resolve runtime paths from project root (#2642)
* fix(harness): resolve runtime paths from project root

* docs(config): update

* fix(config): address runtime path review feedback

* test(config): fix skills path e2e root

* test(config): cover legacy config fallback when project root lacks config files

Verifies that when DEER_FLOW_PROJECT_ROOT is unset and cwd has no
config.yaml/extensions_config.json, AppConfig and ExtensionsConfig fall back
to the legacy backend/repo-root candidates — the backward-compat path
requested in PR #2642 review.

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-05-01 22:19:50 +08:00

146 lines
5.7 KiB
Python

"""Runtime path policy tests for standalone harness usage."""
from pathlib import Path
import pytest
import yaml
from deerflow.config import app_config as app_config_module
from deerflow.config import extensions_config as extensions_config_module
from deerflow.config.app_config import AppConfig
from deerflow.config.extensions_config import ExtensionsConfig
from deerflow.config.paths import Paths
from deerflow.config.runtime_paths import project_root
from deerflow.config.skills_config import SkillsConfig
from deerflow.skills.storage import get_or_new_skill_storage
def _clear_path_env(monkeypatch):
for name in (
"DEER_FLOW_CONFIG_PATH",
"DEER_FLOW_EXTENSIONS_CONFIG_PATH",
"DEER_FLOW_HOME",
"DEER_FLOW_PROJECT_ROOT",
"DEER_FLOW_SKILLS_PATH",
):
monkeypatch.delenv(name, raising=False)
def test_default_runtime_paths_resolve_from_current_project(tmp_path: Path, monkeypatch):
_clear_path_env(monkeypatch)
monkeypatch.chdir(tmp_path)
(tmp_path / "config.yaml").write_text(
yaml.safe_dump({"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"}}),
encoding="utf-8",
)
(tmp_path / "extensions_config.json").write_text('{"mcpServers": {}, "skills": {}}', encoding="utf-8")
assert AppConfig.resolve_config_path() == tmp_path / "config.yaml"
assert ExtensionsConfig.resolve_config_path() == tmp_path / "extensions_config.json"
assert Paths().base_dir == tmp_path / ".deer-flow"
assert SkillsConfig().get_skills_path() == tmp_path / "skills"
assert get_or_new_skill_storage(skills_path=SkillsConfig().get_skills_path()).get_skills_root_path() == tmp_path / "skills"
def test_deer_flow_project_root_overrides_current_directory(tmp_path: Path, monkeypatch):
_clear_path_env(monkeypatch)
project_root = tmp_path / "project"
other_cwd = tmp_path / "other"
project_root.mkdir()
other_cwd.mkdir()
monkeypatch.chdir(other_cwd)
monkeypatch.setenv("DEER_FLOW_PROJECT_ROOT", str(project_root))
(project_root / "config.yaml").write_text(
yaml.safe_dump({"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"}}),
encoding="utf-8",
)
(project_root / "mcp_config.json").write_text('{"mcpServers": {}, "skills": {}}', encoding="utf-8")
assert AppConfig.resolve_config_path() == project_root / "config.yaml"
assert ExtensionsConfig.resolve_config_path() == project_root / "mcp_config.json"
assert Paths().base_dir == project_root / ".deer-flow"
assert SkillsConfig(path="custom-skills").get_skills_path() == project_root / "custom-skills"
def test_deer_flow_skills_path_overrides_project_default(tmp_path: Path, monkeypatch):
_clear_path_env(monkeypatch)
monkeypatch.chdir(tmp_path)
monkeypatch.setenv("DEER_FLOW_SKILLS_PATH", "team-skills")
assert SkillsConfig().get_skills_path() == tmp_path / "team-skills"
assert get_or_new_skill_storage(skills_path=SkillsConfig().get_skills_path()).get_skills_root_path() == tmp_path / "team-skills"
def test_deer_flow_project_root_must_exist(tmp_path: Path, monkeypatch):
_clear_path_env(monkeypatch)
missing_root = tmp_path / "missing"
monkeypatch.setenv("DEER_FLOW_PROJECT_ROOT", str(missing_root))
with pytest.raises(ValueError, match="does not exist"):
project_root()
def test_deer_flow_project_root_must_be_directory(tmp_path: Path, monkeypatch):
_clear_path_env(monkeypatch)
project_root_file = tmp_path / "project-root"
project_root_file.write_text("", encoding="utf-8")
monkeypatch.setenv("DEER_FLOW_PROJECT_ROOT", str(project_root_file))
with pytest.raises(ValueError, match="not a directory"):
project_root()
def test_app_config_falls_back_to_legacy_when_project_root_lacks_config(tmp_path: Path, monkeypatch):
"""When DEER_FLOW_PROJECT_ROOT is unset and cwd has no config.yaml, the
legacy backend/repo-root candidates must be used for monorepo compatibility."""
_clear_path_env(monkeypatch)
cwd = tmp_path / "cwd"
cwd.mkdir()
monkeypatch.chdir(cwd)
legacy_backend = tmp_path / "legacy-backend"
legacy_repo = tmp_path / "legacy-repo"
legacy_backend.mkdir()
legacy_repo.mkdir()
legacy_backend_config = legacy_backend / "config.yaml"
legacy_backend_config.write_text(
yaml.safe_dump({"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"}}),
encoding="utf-8",
)
repo_root_config = legacy_repo / "config.yaml"
repo_root_config.write_text("", encoding="utf-8")
monkeypatch.setattr(
app_config_module,
"_legacy_config_candidates",
lambda: (legacy_backend_config, repo_root_config),
)
assert AppConfig.resolve_config_path() == legacy_backend_config
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
the caller project root has no extensions_config.json/mcp_config.json."""
_clear_path_env(monkeypatch)
cwd = tmp_path / "cwd"
cwd.mkdir()
monkeypatch.chdir(cwd)
fake_backend = tmp_path / "fake-backend"
fake_repo = tmp_path / "fake-repo"
fake_backend.mkdir()
fake_repo.mkdir()
legacy_extensions = fake_backend / "extensions_config.json"
legacy_extensions.write_text('{"mcpServers": {}, "skills": {}}', encoding="utf-8")
fake_paths_module_file = fake_backend / "packages" / "harness" / "deerflow" / "config" / "extensions_config.py"
fake_paths_module_file.parent.mkdir(parents=True)
fake_paths_module_file.write_text("", encoding="utf-8")
monkeypatch.setattr(extensions_config_module, "__file__", str(fake_paths_module_file))
assert ExtensionsConfig.resolve_config_path() == legacy_extensions