deer-flow/backend/tests/test_deploy_uv_extras.py
Janlay 72f033fbbe
feat(gateway): add redis stream bridge (#3191)
* feat: add redis stream bridge

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix(gateway): address redis stream bridge review

Redis was imported eagerly through deerflow.runtime and declared as a hard dependency, which made memory-only installs load redis.asyncio at startup and left the lazy factory import ineffective. Move redis behind an optional extra, remove the public eager re-export, and keep make_stream_bridge as the only runtime import path with an actionable install hint when the extra is missing.

Because Docker deployments now default the stream bridge to Redis via DEER_FLOW_STREAM_BRIDGE_REDIS_URL, install the redis extra explicitly in Docker/dev container flows and teach the local uv-extra detector to infer redis from both stream_bridge.type and the Redis URL env var. This keeps Docker working while preserving slim non-Docker installs.

Harden the Redis bridge by batching XREAD replay, replacing brittle ResponseError string matching with a single fallback to 0-0 for malformed Last-Event-ID values, documenting connection/retention/fail-hard behavior, and adding fake plus opt-in real Redis coverage for XADD/XREAD, replay, invalid IDs, and MAXLEN trimming.

* fix(config): bump config version for stream bridge

* fix redis stream bridge terminal handling

* fix: repair uv.lock, format redis.py, and align Dockerfile extras test

The uv.lock file was missing a closing bracket for the redis extras
section, redis.py had a formatting issue caught by ruff, and the
Dockerfile extras test did not account for the hardcoded --extra redis
flag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-07-04 09:21:19 +08:00

229 lines
7.3 KiB
Python

"""Regression coverage for production deploy.sh UV_EXTRAS propagation."""
from __future__ import annotations
import os
import re
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
def _backend_dockerfile_uv_sync_script() -> str:
dockerfile = (REPO_ROOT / "backend" / "Dockerfile").read_text(encoding="utf-8")
match = re.search(r"""sh -c (?P<quote>["'])(?P<script>.*?uv sync.*?)(?P=quote)""", dockerfile, re.S)
assert match is not None
return match.group("script").replace("\\\n", "\n")
def test_backend_dockerfile_expands_multiple_uv_extras(tmp_path):
"""Dockerfile build args must become repeated uv --extra flags."""
workdir = tmp_path / "work"
backend = workdir / "backend"
backend.mkdir(parents=True)
capture = tmp_path / "uv_args.txt"
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
uv = bin_dir / "uv"
uv.write_text(
'#!/usr/bin/env sh\nfor arg in "$@"; do printf "%s\\n" "$arg"; done > "$CAPTURE_UV_ARGS"\n',
encoding="utf-8",
)
uv.chmod(0o755)
env = os.environ.copy()
env["CAPTURE_UV_ARGS"] = str(capture)
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
env["UV_EXTRAS"] = "discord,postgres"
subprocess.run(
["sh", "-c", _backend_dockerfile_uv_sync_script()],
cwd=workdir,
env=env,
check=True,
)
assert capture.read_text(encoding="utf-8").splitlines() == [
"sync",
"--extra",
"redis",
"--extra",
"discord",
"--extra",
"postgres",
]
def test_backend_dockerfile_rejects_glob_uv_extra(tmp_path):
"""Dockerfile extras must reject globs before invoking uv."""
workdir = tmp_path / "work"
backend = workdir / "backend"
backend.mkdir(parents=True)
(backend / "glob-match").touch()
capture = tmp_path / "uv_args.txt"
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
uv = bin_dir / "uv"
uv.write_text(
'#!/usr/bin/env sh\nfor arg in "$@"; do printf "%s\\n" "$arg"; done > "$CAPTURE_UV_ARGS"\n',
encoding="utf-8",
)
uv.chmod(0o755)
env = os.environ.copy()
env["CAPTURE_UV_ARGS"] = str(capture)
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
env["UV_EXTRAS"] = "postgres,*"
result = subprocess.run(
["sh", "-c", _backend_dockerfile_uv_sync_script()],
cwd=workdir,
env=env,
check=False,
capture_output=True,
text=True,
)
assert result.returncode != 0
assert not capture.exists()
def test_deploy_build_auto_detects_postgres_extra_when_other_extras_are_enabled(tmp_path):
"""Production image builds preserve every detected extra as Docker build tokens."""
worktree = tmp_path / "repo"
shutil.copytree(REPO_ROOT / "scripts", worktree / "scripts")
shutil.copytree(REPO_ROOT / "docker", worktree / "docker")
(worktree / "backend").mkdir()
(worktree / "config.yaml").write_text(
"database:\n backend: postgres\nchannels:\n discord:\n enabled: true\n",
encoding="utf-8",
)
(worktree / "extensions_config.json").write_text('{"mcpServers":{},"skills":{}}\n', encoding="utf-8")
capture = tmp_path / "uv_extras.txt"
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
docker = bin_dir / "docker"
docker.write_text(
'#!/usr/bin/env sh\nprintf "%s" "${UV_EXTRAS:-}" > "$CAPTURE_UV_EXTRAS"\nexit 0\n',
encoding="utf-8",
)
docker.chmod(0o755)
env = os.environ.copy()
env.pop("UV_EXTRAS", None)
env["CAPTURE_UV_EXTRAS"] = str(capture)
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
subprocess.run(
["bash", str(worktree / "scripts" / "deploy.sh"), "build"],
cwd=worktree,
env=env,
check=True,
text=True,
capture_output=True,
)
assert capture.read_text(encoding="utf-8") == "discord,postgres"
def test_deploy_uses_dotenv_without_sourcing_shell_syntax(tmp_path):
"""Repo-root .env is Docker Compose dotenv, not a shell script."""
worktree = tmp_path / "repo"
shutil.copytree(REPO_ROOT / "scripts", worktree / "scripts")
shutil.copytree(REPO_ROOT / "docker", worktree / "docker")
(worktree / "backend").mkdir()
(worktree / "config.yaml").write_text(
"database:\n backend: postgres\n",
encoding="utf-8",
)
(worktree / "extensions_config.json").write_text('{"mcpServers":{},"skills":{}}\n', encoding="utf-8")
marker = tmp_path / "sourced-marker"
(worktree / ".env").write_text(
f"DATABASE_URL=postgresql://user:pass@localhost/db?sslmode=require&application_name=deer\nUNSAFE=$(touch {shlex.quote(str(marker))})\nUV_EXTRAS=discord\n",
encoding="utf-8",
)
capture_extras = tmp_path / "uv_extras.txt"
capture_args = tmp_path / "docker_args.txt"
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
docker = bin_dir / "docker"
docker.write_text(
'#!/usr/bin/env sh\nprintf "%s" "${UV_EXTRAS:-}" > "$CAPTURE_UV_EXTRAS"\nfor arg in "$@"; do printf "%s\\n" "$arg"; done > "$CAPTURE_DOCKER_ARGS"\nexit 0\n',
encoding="utf-8",
)
docker.chmod(0o755)
env = os.environ.copy()
env.pop("UV_EXTRAS", None)
env["CAPTURE_UV_EXTRAS"] = str(capture_extras)
env["CAPTURE_DOCKER_ARGS"] = str(capture_args)
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
subprocess.run(
["bash", str(worktree / "scripts" / "deploy.sh"), "build"],
cwd=worktree,
env=env,
check=True,
text=True,
capture_output=True,
)
assert not marker.exists()
assert capture_extras.read_text(encoding="utf-8") == "discord"
args = capture_args.read_text(encoding="utf-8").splitlines()
assert "--env-file" in args
assert str(worktree / ".env") in args
def test_deploy_build_auto_detects_postgres_extra_with_python_fallback(tmp_path):
"""Production deploy hosts may have python but no runnable python3."""
worktree = tmp_path / "repo"
shutil.copytree(REPO_ROOT / "scripts", worktree / "scripts")
shutil.copytree(REPO_ROOT / "docker", worktree / "docker")
(worktree / "backend").mkdir()
(worktree / "config.yaml").write_text(
"database:\n backend: postgres\n",
encoding="utf-8",
)
(worktree / "extensions_config.json").write_text('{"mcpServers":{},"skills":{}}\n', encoding="utf-8")
capture = tmp_path / "uv_extras.txt"
bin_dir = tmp_path / "bin"
bin_dir.mkdir()
docker = bin_dir / "docker"
docker.write_text(
'#!/usr/bin/env sh\nprintf "%s" "${UV_EXTRAS:-}" > "$CAPTURE_UV_EXTRAS"\nexit 0\n',
encoding="utf-8",
)
docker.chmod(0o755)
python3 = bin_dir / "python3"
python3.write_text("#!/usr/bin/env sh\nexit 1\n", encoding="utf-8")
python3.chmod(0o755)
python = bin_dir / "python"
python.write_text(
f'#!/usr/bin/env sh\nexec {shlex.quote(sys.executable)} "$@"\n',
encoding="utf-8",
)
python.chmod(0o755)
env = os.environ.copy()
env.pop("UV_EXTRAS", None)
env["CAPTURE_UV_EXTRAS"] = str(capture)
env["PATH"] = f"{bin_dir}{os.pathsep}{env['PATH']}"
subprocess.run(
["bash", str(worktree / "scripts" / "deploy.sh"), "build"],
cwd=worktree,
env=env,
check=True,
text=True,
capture_output=True,
)
assert capture.read_text(encoding="utf-8") == "postgres"