mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* fix(make): name what make clean deletes and refuse under a live gateway `make help` described `make clean` as cleaning up "temporary files", but it deletes backend/.deer-flow: the local database, users, threads, uploads, memory, and secrets. Both Docker stacks mount that directory into the deer-flow-gateway container, and `make stop` only stops local services, so the data could be deleted under a running Gateway. Say what is deleted in the help text and before deleting, and refuse while the deer-flow-gateway container is running. The check runs before `make stop`, which would otherwise stop the running stack's sandboxes first; a missing or unreachable Docker passes. * docs(changelog): reference #6351 for the make clean guard * fix(make): share make clean's data list and pin stop before the delete The help line listed "database, threads, uploads, secrets" while the deletion notice also named users and memory. Define the list once in RUNTIME_DATA_CONTENTS and use it in both. Running sandbox containers left behind by `make down` are already stopped by `make stop` (serve.sh --stop reaps deer-flow-sandbox*), which runs after the guard and before the delete. Note that in the guard script, and pin the ordering: the stub stop now records that the data still exists. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
133 lines
5.4 KiB
Python
133 lines
5.4 KiB
Python
"""`make clean` says what it deletes and refuses while the Gateway container runs.
|
|
|
|
The target deletes ``backend/.deer-flow`` -- the database, users, threads,
|
|
uploads, and secrets -- but ``make help`` described it as cleaning up
|
|
"temporary files". Both Docker stacks mount that directory into the
|
|
``deer-flow-gateway`` container, and ``make stop`` only stops local services,
|
|
so ``make clean`` used to delete the data under a running Gateway.
|
|
|
|
These tests run the real ``make clean`` from the real Makefile and guard script
|
|
in a scratch checkout. ``scripts/serve.sh`` is a stub that records the stop
|
|
call, so nothing touches the host's processes or containers, and ``docker`` is
|
|
a fake on a PATH limited to the tools the recipe needs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
|
|
pytestmark = pytest.mark.skipif(
|
|
os.name == "nt" or shutil.which("make") is None or shutil.which("bash") is None,
|
|
reason="runs the POSIX make recipe",
|
|
)
|
|
|
|
_TOOLS = ("make", "bash", "sh", "rm", "grep", "tr")
|
|
|
|
|
|
def _checkout(tmp_path: Path) -> Path:
|
|
root = tmp_path / "checkout"
|
|
(root / "scripts").mkdir(parents=True)
|
|
shutil.copy2(REPO_ROOT / "Makefile", root / "Makefile")
|
|
shutil.copy2(REPO_ROOT / "scripts" / "check-data-not-in-use.sh", root / "scripts" / "check-data-not-in-use.sh")
|
|
# Records each stop call and whether the data still existed at that point:
|
|
# the real serve.sh --stop also stops deer-flow-sandbox* containers, which
|
|
# bind-mount thread directories under backend/.deer-flow.
|
|
(root / "scripts" / "serve.sh").write_text(
|
|
'#!/usr/bin/env bash\nif [ -e backend/.deer-flow ]; then state=data-present; else state=data-gone; fi\necho "$* $state" >> stop-calls\n',
|
|
encoding="utf-8",
|
|
)
|
|
data = root / "backend" / ".deer-flow" / "data"
|
|
data.mkdir(parents=True)
|
|
(data / "deerflow.db").write_text("db", encoding="utf-8")
|
|
(root / "logs").mkdir()
|
|
(root / "logs" / "gateway.log").write_text("log", encoding="utf-8")
|
|
return root
|
|
|
|
|
|
def _bin(tmp_path: Path, docker: str | None) -> Path:
|
|
bin_dir = tmp_path / "bin"
|
|
bin_dir.mkdir()
|
|
for tool in _TOOLS:
|
|
found = shutil.which(tool)
|
|
assert found, tool
|
|
(bin_dir / tool).symlink_to(found)
|
|
if docker is not None:
|
|
fake = bin_dir / "docker"
|
|
fake.write_text(f"#!/bin/sh\n{docker}\n", encoding="utf-8")
|
|
fake.chmod(0o755)
|
|
return bin_dir
|
|
|
|
|
|
def _make(root: Path, bin_dir: Path, target: str = "clean") -> subprocess.CompletedProcess[str]:
|
|
env = {"PATH": str(bin_dir), "HOME": str(root)}
|
|
return subprocess.run(["make", "-s", target], cwd=root, env=env, capture_output=True, text=True, timeout=60)
|
|
|
|
|
|
def test_help_says_clean_deletes_runtime_data(tmp_path: Path) -> None:
|
|
root = _checkout(tmp_path)
|
|
|
|
help_line = next(line for line in _make(root, _bin(tmp_path, None), "help").stdout.splitlines() if "make clean" in line)
|
|
|
|
assert "temporary files" not in help_line
|
|
assert "DELETE" in help_line and "backend/.deer-flow" in help_line
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"names",
|
|
["deer-flow-gateway", "deer-flow-redis\ndeer-flow-gateway", "deer-flow-gateway\r"],
|
|
ids=["gateway", "whole-stack", "crlf"],
|
|
)
|
|
def test_clean_refuses_while_the_gateway_container_runs(tmp_path: Path, names: str) -> None:
|
|
root = _checkout(tmp_path)
|
|
listing = names.replace("\n", "\\n").replace("\r", "\\r")
|
|
|
|
result = _make(root, _bin(tmp_path, f"printf '{listing}\\n'"))
|
|
|
|
assert result.returncode != 0
|
|
assert "deer-flow-gateway container is running" in result.stderr
|
|
assert (root / "backend" / ".deer-flow" / "data" / "deerflow.db").exists()
|
|
assert (root / "logs" / "gateway.log").exists()
|
|
# The guard runs before stop, which would stop the stack's sandboxes.
|
|
assert not (root / "stop-calls").exists()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"docker",
|
|
[None, "exit 1", "printf 'deer-flow-gateway-old\\nmy-deer-flow-gateway\\ndeer-flow-sandbox-1\\n'"],
|
|
ids=["no-docker", "daemon-down", "other-containers"],
|
|
)
|
|
def test_clean_deletes_runtime_data_when_no_gateway_container_runs(tmp_path: Path, docker: str | None) -> None:
|
|
root = _checkout(tmp_path)
|
|
|
|
result = _make(root, _bin(tmp_path, docker))
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert "Deleting local runtime data in backend/.deer-flow" in result.stdout
|
|
assert not (root / "backend" / ".deer-flow").exists()
|
|
assert not (root / "logs" / "gateway.log").exists()
|
|
# stop (and with it the sandbox-container cleanup) runs before the deletion.
|
|
assert (root / "stop-calls").read_text(encoding="utf-8").split() == ["--stop", "data-present"]
|
|
|
|
|
|
def _listed_contents(line: str, prefix: str) -> list[str]:
|
|
return line.split(prefix, 1)[1].split(")", 1)[0].split(", ")
|
|
|
|
|
|
def test_help_and_deletion_notice_list_the_same_data(tmp_path: Path) -> None:
|
|
root = _checkout(tmp_path)
|
|
bin_dir = _bin(tmp_path, None)
|
|
|
|
help_line = next(line for line in _make(root, bin_dir, "help").stdout.splitlines() if "make clean" in line)
|
|
notice = next(line for line in _make(root, bin_dir).stdout.splitlines() if line.startswith("Deleting local runtime data"))
|
|
|
|
listed = _listed_contents(help_line, "backend/.deer-flow: ")
|
|
assert listed == _listed_contents(notice, "backend/.deer-flow (")
|
|
assert {"database", "users", "threads", "uploads", "memory", "secrets"} <= set(listed)
|