mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(scripts): honor project root when detecting optional extras (#6934)
* fix(scripts): honor project root when detecting optional extras * fix(scripts): admit project-root config during startup --------- Co-authored-by: wxhking <186391338+wxhking@users.noreply.github.com>
This commit is contained in:
parent
63ccc6ebfb
commit
9b3397a0ea
@ -234,7 +234,7 @@ streams simulate chunks from a non-streaming response, while no-tool streams sta
|
||||
Without an explicit home, a configured `DEER_FLOW_PROJECT_ROOT` selects its
|
||||
`.deer-flow` directory; external paths use that variable name in the report.
|
||||
|
||||
> **Advanced / manual configuration**: If you prefer to edit `config.yaml` directly, run `make config` instead to copy the full template. Optional dependency auto-detection accepts UTF-8 configuration files with or without a byte-order mark (BOM). See `config.example.yaml` for the complete reference including CLI-backed providers (Codex CLI, Claude Code OAuth), OpenRouter, Responses API, subagent runtime caps such as `subagents.max_total_per_run`, and more.
|
||||
> **Advanced / manual configuration**: If you prefer to edit `config.yaml` directly, run `make config` instead to copy the full template. Optional dependency auto-detection accepts UTF-8 configuration files with or without a byte-order mark (BOM). It checks an existing `DEER_FLOW_CONFIG_PATH` first, then `DEER_FLOW_PROJECT_ROOT/config.yaml`, before the checkout's default locations. Local startup also accepts the project-root configuration when the checkout has no `config.yaml`. See `config.example.yaml` for the complete reference including CLI-backed providers (Codex CLI, Claude Code OAuth), OpenRouter, Responses API, subagent runtime caps such as `subagents.max_total_per_run`, and more.
|
||||
|
||||
Optional per-model pricing must use one currency across all priced models.
|
||||
DeerFlow disables Console cost estimates when currencies are mixed rather
|
||||
|
||||
@ -7,6 +7,8 @@ future opt-in extras) are not wiped on every restart — see Issue #2754.
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
@ -27,7 +29,9 @@ def isolated_cwd(tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
monkeypatch.delenv("UV_EXTRAS", raising=False)
|
||||
monkeypatch.delenv("DEER_FLOW_CONFIG_PATH", raising=False)
|
||||
monkeypatch.delenv("DEER_FLOW_PROJECT_ROOT", raising=False)
|
||||
monkeypatch.delenv("DEER_FLOW_STREAM_BRIDGE_REDIS_URL", raising=False)
|
||||
monkeypatch.delenv("DEER_FLOW_SANDBOX_OWNERSHIP_REDIS_URL", raising=False)
|
||||
return tmp_path
|
||||
|
||||
|
||||
@ -498,3 +502,44 @@ def test_resolve_extras_root_config_takes_precedence(isolated_cwd):
|
||||
(sub / "config.yaml").write_text("database:\n backend: postgres\n")
|
||||
# Root config.yaml is checked first, matching the precedence in serve.sh.
|
||||
assert detect.resolve_extras() == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkout_config", ["config.yaml", "backend/config.yaml", None])
|
||||
def test_resolve_extras_prefers_project_root_config(isolated_cwd, monkeypatch, checkout_config):
|
||||
if checkout_config is not None:
|
||||
local = isolated_cwd / checkout_config
|
||||
local.parent.mkdir(parents=True, exist_ok=True)
|
||||
local.write_text("database:\n backend: sqlite\n", encoding="utf-8")
|
||||
project = isolated_cwd / "external project"
|
||||
project.mkdir()
|
||||
config = project / "config.yaml"
|
||||
config.write_text("database:\n backend: postgres\nmodels:\n - use: langchain_ollama:ChatOllama\n", encoding="utf-8")
|
||||
monkeypatch.setenv("DEER_FLOW_PROJECT_ROOT", str(project))
|
||||
|
||||
assert detect.find_config_file() == config
|
||||
assert detect.resolve_extras() == ["ollama", "postgres"]
|
||||
# The detector runs before dependency sync, so its CLI must work without site packages.
|
||||
result = subprocess.run([sys.executable, "-S", str(DETECT_SCRIPT_PATH)], capture_output=True, text=True, check=True)
|
||||
assert result.stdout == "--extra ollama --extra postgres"
|
||||
|
||||
|
||||
def test_resolve_extras_explicit_config_overrides_project_root(isolated_cwd, monkeypatch):
|
||||
project = isolated_cwd / "external project"
|
||||
project.mkdir()
|
||||
(project / "config.yaml").write_text("database:\n backend: postgres\n", encoding="utf-8")
|
||||
explicit = isolated_cwd / "explicit.yaml"
|
||||
explicit.write_text("stream_bridge:\n type: redis\n", encoding="utf-8")
|
||||
monkeypatch.setenv("DEER_FLOW_PROJECT_ROOT", str(project))
|
||||
monkeypatch.setenv("DEER_FLOW_CONFIG_PATH", str(explicit))
|
||||
|
||||
assert detect.find_config_file() == explicit
|
||||
assert detect.resolve_extras() == ["redis"]
|
||||
|
||||
|
||||
def test_resolve_extras_project_without_config_keeps_legacy_fallback(isolated_cwd, monkeypatch):
|
||||
project = isolated_cwd / "external project"
|
||||
project.mkdir()
|
||||
(isolated_cwd / "config.yaml").write_text("database:\n backend: postgres\n", encoding="utf-8")
|
||||
monkeypatch.setenv("DEER_FLOW_PROJECT_ROOT", str(project))
|
||||
|
||||
assert detect.resolve_extras() == ["postgres"]
|
||||
|
||||
102
backend/tests/test_serve_config_presence.py
Normal file
102
backend/tests/test_serve_config_presence.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""Exercise startup config admission before real optional dependency detection."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from support.shell import require_script_bash
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
# Keep the complete launcher and detector, replacing process/service operations.
|
||||
# Stop at dependency synchronization, before any service can be launched.
|
||||
_PRELUDE = """
|
||||
git() { return 1; }
|
||||
lsof() { return 1; }
|
||||
pgrep() { return 1; }
|
||||
nginx() { return 0; }
|
||||
sleep() { return 0; }
|
||||
python3() { "$TEST_PYTHON" "$@"; }
|
||||
env() {
|
||||
if [ "$1" = python3 ]; then shift; python3 "$@"; else command env "$@"; fi
|
||||
}
|
||||
uv() { printf '%s\\n' "$@" > "$TEST_UV_ARGS"; return 73; }
|
||||
source "$1" "$2"
|
||||
"""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", ["--dev", "--prod"])
|
||||
@pytest.mark.parametrize("config_source", ["project-env", "project-dotenv", "explicit", "checkout", "legacy", "missing", "project-directory"])
|
||||
def test_serve_config_presence_reaches_dependency_sync(tmp_path: Path, mode: str, config_source: str) -> None:
|
||||
bash = require_script_bash()
|
||||
checkout = tmp_path / "checkout"
|
||||
scripts = checkout / "scripts"
|
||||
scripts.mkdir(parents=True)
|
||||
(checkout / "backend").mkdir()
|
||||
for name in ("serve.sh", "detect_uv_extras.py"):
|
||||
shutil.copy2(REPO_ROOT / "scripts" / name, scripts / name)
|
||||
(scripts / "nginx-local-conf.sh").write_text("printf 'test-nginx.conf\\n'\n", encoding="utf-8")
|
||||
(scripts / "cleanup-containers.sh").write_text("exit 0\n", encoding="utf-8")
|
||||
(scripts / "config-upgrade.sh").write_text('printf upgraded > "$TEST_UPGRADE_MARKER"\n', encoding="utf-8")
|
||||
|
||||
env = os.environ.copy()
|
||||
for key in (
|
||||
"DEER_FLOW_CONFIG_PATH",
|
||||
"DEER_FLOW_PROJECT_ROOT",
|
||||
"DEER_FLOW_HOME",
|
||||
"DEER_FLOW_ENV_FILE",
|
||||
"DEER_FLOW_STREAM_BRIDGE_REDIS_URL",
|
||||
"DEER_FLOW_SANDBOX_OWNERSHIP_REDIS_URL",
|
||||
"UV_EXTRAS",
|
||||
"BASH_ENV",
|
||||
"ENV",
|
||||
):
|
||||
env.pop(key, None)
|
||||
project = tmp_path / "external project"
|
||||
project.mkdir()
|
||||
env["DEER_FLOW_PROJECT_ROOT"] = project.as_posix()
|
||||
expected_extras = []
|
||||
if config_source in {"project-env", "project-dotenv"}:
|
||||
(project / "config.yaml").write_text("models:\n - use: langchain_ollama:ChatOllama\n", encoding="utf-8")
|
||||
expected_extras = ["--extra", "ollama"]
|
||||
if config_source == "project-dotenv":
|
||||
env.pop("DEER_FLOW_PROJECT_ROOT")
|
||||
(checkout / ".env").write_text(f"DEER_FLOW_PROJECT_ROOT={shlex.quote(project.as_posix())}\n", encoding="utf-8")
|
||||
elif config_source == "project-directory":
|
||||
(project / "config.yaml").mkdir()
|
||||
elif config_source != "missing":
|
||||
config = {"explicit": tmp_path / "explicit.yaml", "checkout": checkout / "config.yaml", "legacy": checkout / "backend" / "config.yaml"}[config_source]
|
||||
config.write_text("database:\n backend: sqlite\n", encoding="utf-8")
|
||||
if config_source == "explicit":
|
||||
env["DEER_FLOW_CONFIG_PATH"] = config.as_posix()
|
||||
|
||||
marker = tmp_path / "upgrade.txt"
|
||||
uv_args = tmp_path / "uv-args.txt"
|
||||
env.update(TEST_PYTHON=Path(sys.executable).as_posix(), TEST_UPGRADE_MARKER=marker.as_posix(), TEST_UV_ARGS=uv_args.as_posix())
|
||||
result = subprocess.run(
|
||||
[bash, "-c", _PRELUDE, "startup-config-test", (scripts / "serve.sh").as_posix(), mode],
|
||||
cwd=checkout,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=30,
|
||||
check=False,
|
||||
)
|
||||
|
||||
assert result.returncode == 1, result.stdout + result.stderr
|
||||
if config_source in {"missing", "project-directory"}:
|
||||
assert "No DeerFlow config file found" in result.stdout
|
||||
assert not marker.exists()
|
||||
assert not uv_args.exists()
|
||||
else:
|
||||
assert "No DeerFlow config file found" not in result.stdout, result.stdout + result.stderr
|
||||
assert marker.read_text(encoding="utf-8") == "upgraded"
|
||||
assert uv_args.read_text(encoding="utf-8").splitlines() == ["sync", "--locked", "--quiet", "--all-packages", *expected_extras]
|
||||
assert "Backend dependency install failed" in result.stdout
|
||||
@ -72,6 +72,13 @@ indentless lists are supported; nested option names and block-scalar text
|
||||
must not enable the browser extra. Keep the detector standard-library-only
|
||||
because it runs before dependency synchronization. Read UTF-8 config files
|
||||
with or without a leading BOM so the first section remains detectable.
|
||||
`detect_uv_extras.py` checks an existing `DEER_FLOW_CONFIG_PATH` first, then
|
||||
`DEER_FLOW_PROJECT_ROOT/config.yaml`, before the legacy checkout candidates.
|
||||
Keep this lookup standard-library-only; test shadowing checkout configs and
|
||||
the CLI under `python -S` in `backend/tests/test_detect_uv_extras.py`.
|
||||
`serve.sh`'s config-presence gate also accepts the project-root file when the
|
||||
checkout has no config. `tests/test_serve_config_presence.py` runs the full
|
||||
launcher through dependency detection with service operations stubbed.
|
||||
`setup-sandbox.sh` also strips the leading BOM and normalizes CRLF before selecting the image;
|
||||
keep its shell filter compatible with GNU and BSD sed.
|
||||
An Apple Container pull that succeeds on macOS must not fail the setup step
|
||||
|
||||
@ -65,12 +65,16 @@ def parse_env_extras(value: str) -> list[str]:
|
||||
|
||||
|
||||
def find_config_file() -> Path | None:
|
||||
"""Locate config.yaml using the same precedence as serve.sh."""
|
||||
"""Prefer explicit config and project-root settings before legacy locations."""
|
||||
explicit = os.environ.get("DEER_FLOW_CONFIG_PATH")
|
||||
if explicit:
|
||||
candidate = Path(explicit)
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
if project_root := os.environ.get("DEER_FLOW_PROJECT_ROOT"):
|
||||
candidate = Path(project_root) / "config.yaml"
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
for path in (Path("config.yaml"), Path("backend/config.yaml")):
|
||||
if path.is_file():
|
||||
return path
|
||||
|
||||
@ -387,6 +387,7 @@ fi
|
||||
|
||||
if ! { \
|
||||
[ -n "$DEER_FLOW_CONFIG_PATH" ] && [ -f "$DEER_FLOW_CONFIG_PATH" ] || \
|
||||
[ -f "$DEER_FLOW_PROJECT_ROOT/config.yaml" ] || \
|
||||
[ -f backend/config.yaml ] || \
|
||||
[ -f config.yaml ]; \
|
||||
}; then
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user