mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-23 00:16:48 +00:00
7976bdf50c
Address review feedback on the previous commit: 1. Narrow exception catch removed. The old contract returned 503 whenever `app.state.config is None`. The first cut only mapped `FileNotFoundError`, leaving `PermissionError`, YAML parse errors, and pydantic `ValidationError` to bubble up as 500. At the request boundary we treat any inability to materialise the config as "configuration not available" (503) and log the original exception so the operator still has the stack. 2. Removed the unused `request: Request` parameter and the matching `# noqa: ARG001`. FastAPI's `Depends()` does not require the dependency to accept `Request`; the only call site uses the no-arg form. 3. `backend/CLAUDE.md` boundary now lists the *reason* each field is restart-required (engine binding, singleton caching, one-shot `apply_logging_level`, etc.), not just the field name, so reviewers do not have to reverse-engineer the boundary themselves. Tests parametrise four exception classes (`FileNotFoundError`, `PermissionError`, `ValueError`, `RuntimeError`) and assert 503 for each. Refs: bytedance/deer-flow#3107 (BUG-001)
144 lines
4.7 KiB
Python
144 lines
4.7 KiB
Python
"""Regression tests for gateway config freshness on the request hot path.
|
|
|
|
Bytedance/deer-flow issue #3107 BUG-001: the worker and lead-agent path
|
|
captured ``app.state.config`` at gateway startup. ``config.yaml`` edits during
|
|
runtime were therefore ignored — ``get_app_config()``'s mtime-based reload
|
|
existed but was bypassed because the snapshot object was passed through
|
|
explicitly.
|
|
|
|
These tests pin the desired behaviour: a request-time ``get_config`` call must
|
|
observe the most recent on-disk ``config.yaml`` (mtime reload), and the
|
|
runtime ``ContextVar`` override must keep working for per-request injection.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import Depends, FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app.gateway import deps as gateway_deps
|
|
from app.gateway.deps import get_config
|
|
from deerflow.config.app_config import (
|
|
AppConfig,
|
|
pop_current_app_config,
|
|
push_current_app_config,
|
|
reset_app_config,
|
|
set_app_config,
|
|
)
|
|
from deerflow.config.sandbox_config import SandboxConfig
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_app_config_singleton():
|
|
"""Ensure each test starts with a clean module-level cache."""
|
|
reset_app_config()
|
|
yield
|
|
reset_app_config()
|
|
|
|
|
|
def _write_config_yaml(path: Path, *, log_level: str) -> None:
|
|
path.write_text(
|
|
f"""
|
|
sandbox:
|
|
use: deerflow.sandbox.local.provider:LocalSandboxProvider
|
|
log_level: {log_level}
|
|
""".strip()
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def _build_app() -> FastAPI:
|
|
app = FastAPI()
|
|
|
|
@app.get("/probe")
|
|
def probe(cfg: AppConfig = Depends(get_config)):
|
|
return {"log_level": cfg.log_level}
|
|
|
|
return app
|
|
|
|
|
|
def test_get_config_reflects_file_mtime_reload(tmp_path, monkeypatch):
|
|
"""Editing config.yaml at runtime must be visible to /probe without restart.
|
|
|
|
This is the literal repro for the issue: the gateway must not freeze the
|
|
config to whatever was on disk when the process started.
|
|
"""
|
|
config_file = tmp_path / "config.yaml"
|
|
_write_config_yaml(config_file, log_level="info")
|
|
monkeypatch.setenv("DEER_FLOW_CONFIG_PATH", str(config_file))
|
|
|
|
app = _build_app()
|
|
client = TestClient(app)
|
|
assert client.get("/probe").json() == {"log_level": "info"}
|
|
|
|
# Edit the file and bump its mtime — simulating a maintainer changing
|
|
# max_tokens / model settings in production while the gateway is live.
|
|
_write_config_yaml(config_file, log_level="debug")
|
|
future_mtime = config_file.stat().st_mtime + 5
|
|
os.utime(config_file, (future_mtime, future_mtime))
|
|
|
|
assert client.get("/probe").json() == {"log_level": "debug"}
|
|
|
|
|
|
def test_get_config_respects_runtime_context_override(tmp_path, monkeypatch):
|
|
"""Per-request ``push_current_app_config`` injection must still win."""
|
|
config_file = tmp_path / "config.yaml"
|
|
_write_config_yaml(config_file, log_level="info")
|
|
monkeypatch.setenv("DEER_FLOW_CONFIG_PATH", str(config_file))
|
|
|
|
override = AppConfig(sandbox=SandboxConfig(use="test"), log_level="trace")
|
|
push_current_app_config(override)
|
|
try:
|
|
app = _build_app()
|
|
client = TestClient(app)
|
|
assert client.get("/probe").json() == {"log_level": "trace"}
|
|
finally:
|
|
pop_current_app_config()
|
|
|
|
|
|
def test_get_config_respects_test_set_app_config():
|
|
"""``set_app_config`` (used by upload/skills router tests) keeps working."""
|
|
injected = AppConfig(sandbox=SandboxConfig(use="test"), log_level="warning")
|
|
set_app_config(injected)
|
|
|
|
app = _build_app()
|
|
client = TestClient(app)
|
|
assert client.get("/probe").json() == {"log_level": "warning"}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"exception",
|
|
[
|
|
FileNotFoundError("config.yaml not found"),
|
|
PermissionError("config.yaml not readable"),
|
|
ValueError("invalid config"),
|
|
RuntimeError("yaml parse error"),
|
|
],
|
|
)
|
|
def test_get_config_returns_503_on_any_load_failure(monkeypatch, exception):
|
|
"""Any failure to materialise the config must surface as 503, not 500.
|
|
|
|
Bytedance/deer-flow issue #3107 BUG-001 review: the original snapshot
|
|
contract returned 503 when ``app.state.config is None``. The first cut of
|
|
this fix only mapped ``FileNotFoundError`` to 503, which left
|
|
``PermissionError`` / ``yaml.YAMLError`` / ``ValidationError`` etc. bubbling
|
|
up as 500. Catch every load failure at the request boundary.
|
|
"""
|
|
|
|
def _broken_get_app_config():
|
|
raise exception
|
|
|
|
monkeypatch.setattr(gateway_deps, "get_app_config", _broken_get_app_config)
|
|
|
|
app = _build_app()
|
|
client = TestClient(app, raise_server_exceptions=False)
|
|
response = client.get("/probe")
|
|
|
|
assert response.status_code == 503
|
|
assert response.json() == {"detail": "Configuration not available"}
|