mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-09 17:12:01 +00:00
* fix(dev): create backend/sandbox before uvicorn reload-exclude (#3459) #3426 switched the dev gateway's --reload-exclude patterns to absolute paths. uvicorn only excludes an absolute path directly when it already exists as a directory; otherwise it globs the pattern, and Python 3.12's pathlib raises NotImplementedError("Non-relative patterns are unsupported") for an absolute glob pattern. serve.sh mkdir'd the .deer-flow excludes but not backend/sandbox, so `make dev` crashed on startup on a fresh checkout under Python 3.12 (#3454). docker/dev-entrypoint.sh had the same latent gap. Create backend/sandbox in both launchers so every absolute exclude stays on uvicorn's is_dir() short-circuit. Add a regression test that pins the uvicorn mechanism (crash on missing dir, safe once created) and enforces that every absolute --reload-exclude is mkdir'd before launch. Closes #3459 * test(dev): harden reload-exclude invariant parser against false pass/negatives The launcher invariant test parsed shell with a "mkdir -p" line filter and a substring membership check. Two latent gaps (sub-threshold for this fix, but this code guards a user-facing startup path, so close them): - A `\`-continued multi-line `mkdir` would drop arguments on continuation lines, silently weakening coverage. - Substring membership could false-pass when an exclude is a path-prefix of a different created dir (e.g. `/app/backend/sandbox` "found" inside `/app/backend/sandbox-other`). Fold line-continuations, drop comments, and shlex-tokenize each `mkdir` argument list into an exact set (quotes stripped, `$VAR` literal); assert exact set membership. Same shlex handling for `--reload-exclude` values. Verified the parser still flags the pre-fix missing `backend/sandbox` (RED preserved) and no longer false-passes on a path-prefix. * fix(dev): gitignore backend/sandbox runtime dir + pin mkdir-before-launch Address two review findings on the #3459 fix: - backend/sandbox was described as "gitignored runtime state" but no ignore rule actually matched it. Add an anchored `/sandbox/` to backend/.gitignore (anchored so it does NOT shadow the source package backend/packages/harness/deerflow/sandbox/) so sandbox artifacts created at runtime can't pollute the working tree or be committed by accident. New test asserts content under backend/sandbox is ignored, making the claim verifiable. - The launcher invariant test only proved the sandbox mkdir exists somewhere, not that it runs before uvicorn starts. Add an order test (sandbox mkdir line must precede the `uv run uvicorn` launch) so a future edit can't move the mkdir below the launch and silently reintroduce the crash. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * test(dev): fix reload-exclude parser to handle serve.sh's quoted flag bundle The previous autofix tokenized each whole line with shlex, but serve.sh packs every flag into a single double-quoted `GATEWAY_EXTRA_FLAGS="..."` assignment. shlex collapses that into one token, so no `--reload-exclude` flag is found and `test_launcher_precreates_every_absolute_reload_exclude[scripts/serve.sh]` failed CI with "expected at least one absolute reload-exclude". Parse `--reload-exclude` with a regex that matches a balanced single/double quoted group or a bare token, so the assignment's surrounding `"` is never swallowed into the value. This recovers all three serve.sh excludes (the prior regex also silently dropped the last `$BACKEND_RUNTIME_HOME` because the adjacent closing quote broke shlex) while still covering dev-entrypoint.sh and the space-separated `--reload-exclude <value>` form. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
150 lines
6.6 KiB
Python
150 lines
6.6 KiB
Python
"""Regression coverage for the Gateway-owned LangGraph API runtime."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
|
|
def _read(path: str) -> str:
|
|
return (REPO_ROOT / path).read_text(encoding="utf-8")
|
|
|
|
|
|
def test_root_makefile_no_longer_exposes_transition_gateway_targets():
|
|
makefile = _read("Makefile")
|
|
|
|
assert "dev-pro" not in makefile
|
|
assert "start-pro" not in makefile
|
|
assert "dev-daemon-pro" not in makefile
|
|
assert "start-daemon-pro" not in makefile
|
|
assert "docker-start-pro" not in makefile
|
|
assert "up-pro" not in makefile
|
|
assert not re.search(r"serve\.sh .*--gateway", makefile)
|
|
assert "docker.sh start --gateway" not in makefile
|
|
assert "deploy.sh --gateway" not in makefile
|
|
|
|
|
|
def test_service_launchers_always_use_gateway_runtime():
|
|
operational_files = {
|
|
"scripts/serve.sh": _read("scripts/serve.sh"),
|
|
"scripts/docker.sh": _read("scripts/docker.sh"),
|
|
"scripts/deploy.sh": _read("scripts/deploy.sh"),
|
|
"docker/docker-compose-dev.yaml": _read("docker/docker-compose-dev.yaml"),
|
|
"docker/docker-compose.yaml": _read("docker/docker-compose.yaml"),
|
|
}
|
|
|
|
for path, content in operational_files.items():
|
|
assert "start --gateway" not in content, path
|
|
assert "deploy.sh --gateway" not in content, path
|
|
assert "langgraph dev" not in content, path
|
|
assert "LANGGRAPH_UPSTREAM" not in content, path
|
|
assert "LANGGRAPH_REWRITE" not in content, path
|
|
|
|
|
|
def test_local_dev_gateway_reload_excludes_runtime_state_with_absolute_dirs():
|
|
serve_sh = _read("scripts/serve.sh")
|
|
|
|
assert 'export DEER_FLOW_PROJECT_ROOT="$REPO_ROOT"' in serve_sh
|
|
assert 'BACKEND_RUNTIME_HOME="$REPO_ROOT/backend/.deer-flow"' in serve_sh
|
|
assert 'export DEER_FLOW_HOME="$BACKEND_RUNTIME_HOME"' in serve_sh
|
|
# Every absolute reload-exclude must be pre-created, including backend/sandbox
|
|
# (#3459 / #3454) — see test_uvicorn_reload_exclude.py for the mechanism.
|
|
assert 'mkdir -p "$DEER_FLOW_HOME" "$BACKEND_RUNTIME_HOME" "$REPO_ROOT/backend/sandbox"' in serve_sh
|
|
assert "--reload-exclude='$DEER_FLOW_HOME'" in serve_sh
|
|
assert "--reload-exclude='$BACKEND_RUNTIME_HOME'" in serve_sh
|
|
assert "--reload-exclude='sandbox/'" not in serve_sh
|
|
assert "--reload-exclude='.deer-flow/'" not in serve_sh
|
|
|
|
|
|
def test_backend_container_only_exposes_gateway_port():
|
|
dockerfile = _read("backend/Dockerfile")
|
|
|
|
assert not re.search(r"^EXPOSE\s+.*\b2024\b", dockerfile, re.M)
|
|
assert "langgraph: 2024" not in dockerfile
|
|
assert re.search(r"^EXPOSE\s+8001\b", dockerfile, re.M)
|
|
|
|
|
|
def test_root_makefile_clean_does_not_reference_langgraph_server_cache():
|
|
makefile = _read("Makefile")
|
|
|
|
assert ".langgraph_api" not in makefile
|
|
|
|
|
|
def test_nginx_routes_official_langgraph_prefix_to_gateway_api():
|
|
for path in ("docker/nginx/nginx.local.conf", "docker/nginx/nginx.conf"):
|
|
content = _read(path)
|
|
|
|
assert "/api/langgraph-compat" not in content
|
|
assert "proxy_pass http://langgraph" not in content
|
|
assert "rewrite ^/api/langgraph/(.*) /api/$1 break;" in content
|
|
assert "proxy_pass http://gateway" in content or "proxy_pass http://$gateway_upstream" in content
|
|
|
|
|
|
def test_nginx_defers_cors_to_gateway_allowlist():
|
|
for path in ("docker/nginx/nginx.local.conf", "docker/nginx/nginx.conf"):
|
|
content = _read(path)
|
|
|
|
assert "Access-Control-Allow-Origin" not in content
|
|
assert "Access-Control-Allow-Methods" not in content
|
|
assert "Access-Control-Allow-Headers" not in content
|
|
assert "Access-Control-Allow-Credentials" not in content
|
|
assert "proxy_hide_header 'Access-Control-Allow-" not in content
|
|
assert "if ($request_method = 'OPTIONS')" not in content
|
|
|
|
|
|
def test_gateway_cors_configuration_uses_gateway_allowlist():
|
|
gateway_config = _read("backend/app/gateway/config.py")
|
|
gateway_app = _read("backend/app/gateway/app.py")
|
|
csrf_middleware = _read("backend/app/gateway/csrf_middleware.py")
|
|
|
|
assert not re.search(r"(?<!GATEWAY_)[\"']CORS_ORIGINS[\"']", gateway_config)
|
|
assert "cors_origins" not in gateway_config
|
|
assert "get_configured_cors_origins" in gateway_app
|
|
assert "GATEWAY_CORS_ORIGINS" in csrf_middleware
|
|
|
|
|
|
def test_frontend_rewrites_langgraph_prefix_to_gateway():
|
|
next_config = _read("frontend/next.config.js")
|
|
api_client = _read("frontend/src/core/api/api-client.ts")
|
|
|
|
assert "DEER_FLOW_INTERNAL_LANGGRAPH_BASE_URL" not in next_config
|
|
assert "http://127.0.0.1:2024" not in next_config
|
|
assert "langgraph-compat" not in api_client
|
|
|
|
|
|
def test_smoke_test_docs_do_not_expect_standalone_langgraph_server():
|
|
smoke_files = {
|
|
".agent/skills/smoke-test/SKILL.md": _read(".agent/skills/smoke-test/SKILL.md"),
|
|
".agent/skills/smoke-test/references/SOP.md": _read(".agent/skills/smoke-test/references/SOP.md"),
|
|
".agent/skills/smoke-test/references/troubleshooting.md": _read(".agent/skills/smoke-test/references/troubleshooting.md"),
|
|
".agent/skills/smoke-test/scripts/check_local_env.sh": _read(".agent/skills/smoke-test/scripts/check_local_env.sh"),
|
|
".agent/skills/smoke-test/scripts/deploy_local.sh": _read(".agent/skills/smoke-test/scripts/deploy_local.sh"),
|
|
".agent/skills/smoke-test/scripts/health_check.sh": _read(".agent/skills/smoke-test/scripts/health_check.sh"),
|
|
".agent/skills/smoke-test/templates/report.local.template.md": _read(".agent/skills/smoke-test/templates/report.local.template.md"),
|
|
".agent/skills/smoke-test/templates/report.docker.template.md": _read(".agent/skills/smoke-test/templates/report.docker.template.md"),
|
|
}
|
|
|
|
for path, content in smoke_files.items():
|
|
assert "localhost:2024" not in content, path
|
|
assert "127.0.0.1:2024" not in content, path
|
|
assert "deer-flow-langgraph" not in content, path
|
|
assert "langgraph.log" not in content, path
|
|
assert "LangGraph service" not in content, path
|
|
assert "langgraph dev" not in content, path
|
|
|
|
|
|
def test_gateway_runtime_docs_do_not_reference_transition_modes():
|
|
docs = {
|
|
"backend/docs/AUTH_UPGRADE.md": _read("backend/docs/AUTH_UPGRADE.md"),
|
|
"backend/docs/AUTH_TEST_DOCKER_GAP.md": _read("backend/docs/AUTH_TEST_DOCKER_GAP.md"),
|
|
"docs/CODE_CHANGE_SUMMARY_BY_FILE.md": _read("docs/CODE_CHANGE_SUMMARY_BY_FILE.md"),
|
|
}
|
|
|
|
for path, content in docs.items():
|
|
assert "make dev-pro" not in content, path
|
|
assert "./scripts/deploy.sh --gateway" not in content, path
|
|
assert "docker compose --profile gateway" not in content, path
|
|
assert "`/api/langgraph/*` → LangGraph" not in content, path
|