mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(scripts): make doctor.py --help print usage without running diagnostics (#7433)
* fix(scripts): make doctor.py --help print usage without running diagnostics scripts/doctor.py never parsed its arguments: `python scripts/doctor.py --help` printed no usage and ran the whole diagnostic suite (subprocess probes, config resolution, provider checks) and exited 0; any unrecognized flag was silently ignored the same way. Closes #7386. - Parse arguments with argparse.ArgumentParser in the __main__ entry point: --help prints usage and exits 0, unknown flags exit 2. - The no-argument invocation used by `make doctor` is unchanged, and main() stays directly callable from the existing unit tests (they run in-process under pytest, whose argv must not reach argparse). - Add backend/tests/test_doctor_script.py: offline subprocess cases for --help (usage, no diagnostics run), an unknown flag (exit 2), and the no-argument run (report footer present, exit 0 or 1). * test(scripts): isolate doctor CLI subprocess checks --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
041a21b6a0
commit
de03ef97e8
@ -18,6 +18,12 @@ ownership and capture cleanup. Retain the real XLSX conversion coverage.
|
||||
|
||||
Backend tests must preserve the runtime invariants they exercise without changing production execution topology.
|
||||
|
||||
`test_doctor_script.py` runs a copied doctor from a temporary checkout with an
|
||||
empty-model config. Drop inherited `DEER_FLOW_*`, `PYTHONPATH` and `PYTHONHOME`
|
||||
from child processes; cover help, invalid flags, normal reporting and an
|
||||
unrelated parent config that stays untouched. Keep argument parsing at the CLI
|
||||
entry point so in-process `doctor.main()` callers do not consume pytest's argv.
|
||||
|
||||
`test_run_terminal_protocol.py` exercises authoritative `run.terminal`
|
||||
evidence through real Worker finalization. Keep final commit authority,
|
||||
producer drain, singleton confirmation, and Seal boundaries observable. Release all storage/producer gates before bounded cleanup,
|
||||
|
||||
96
backend/tests/test_doctor_script.py
Normal file
96
backend/tests/test_doctor_script.py
Normal file
@ -0,0 +1,96 @@
|
||||
"""CLI contract for scripts/doctor.py, exercised in a disposable checkout."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def doctor_checkout(tmp_path: Path) -> Path:
|
||||
checkout = tmp_path / "checkout"
|
||||
scripts_dir = checkout / "scripts"
|
||||
scripts_dir.mkdir(parents=True)
|
||||
(checkout / "frontend").mkdir()
|
||||
shutil.copyfile(REPO_ROOT / "scripts" / "doctor.py", scripts_dir / "doctor.py")
|
||||
(checkout / "config.yaml").write_text("config_version: 5\nmodels: []\n", encoding="utf-8")
|
||||
return checkout
|
||||
|
||||
|
||||
def _run_doctor(checkout: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
env = {name: value for name, value in os.environ.items() if not name.startswith("DEER_FLOW_") and name not in {"PYTHONPATH", "PYTHONHOME"}}
|
||||
return subprocess.run(
|
||||
[sys.executable, str(checkout / "scripts" / "doctor.py"), *args],
|
||||
cwd=checkout,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
def test_help_prints_usage_without_running_diagnostics(doctor_checkout: Path):
|
||||
result = _run_doctor(doctor_checkout, "--help")
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert "usage:" in result.stdout
|
||||
assert "Status:" not in result.stdout
|
||||
assert "System Requirements" not in result.stdout
|
||||
|
||||
|
||||
def test_unknown_flag_exits_2_with_usage_error(doctor_checkout: Path):
|
||||
result = _run_doctor(doctor_checkout, "--verbos")
|
||||
|
||||
assert result.returncode == 2
|
||||
assert "unrecognized arguments: --verbos" in result.stderr
|
||||
assert "Status:" not in result.stdout
|
||||
assert "System Requirements" not in result.stdout
|
||||
|
||||
|
||||
def test_no_arguments_still_runs_the_diagnostics(doctor_checkout: Path):
|
||||
result = _run_doctor(doctor_checkout)
|
||||
|
||||
# Missing host tools or an incomplete fixture config may produce failures;
|
||||
# either exit code must still come with the normal diagnostic report.
|
||||
assert result.returncode in (0, 1), result.stderr
|
||||
assert "Status:" in result.stdout
|
||||
assert "System Requirements" in result.stdout
|
||||
|
||||
|
||||
def test_no_arguments_ignores_inherited_runtime_configuration(doctor_checkout: Path, tmp_path: Path, monkeypatch):
|
||||
external_root = tmp_path / "external project"
|
||||
external_root.mkdir()
|
||||
external_config = external_root / "config.yaml"
|
||||
config_bytes = b"config_version: 5\nmodels:\n - name: inherited-model\n"
|
||||
external_config.write_bytes(config_bytes)
|
||||
external_env = external_root / ".env"
|
||||
env_bytes = b"# unrelated developer environment\n"
|
||||
external_env.write_bytes(env_bytes)
|
||||
external_home = tmp_path / "external home"
|
||||
for name, value in {
|
||||
"DEER_FLOW_CONFIG_PATH": external_config,
|
||||
"DEER_FLOW_PROJECT_ROOT": external_root,
|
||||
"DEER_FLOW_ENV_FILE": external_env,
|
||||
"DEER_FLOW_HOME": external_home,
|
||||
}.items():
|
||||
monkeypatch.setenv(name, str(value))
|
||||
|
||||
result = _run_doctor(doctor_checkout)
|
||||
|
||||
assert result.returncode in (0, 1), result.stderr
|
||||
assert "Status:" in result.stdout
|
||||
assert "no models found" in result.stdout
|
||||
assert "1 model(s)" not in result.stdout
|
||||
assert external_config.read_bytes() == config_bytes
|
||||
assert external_env.read_bytes() == env_bytes
|
||||
assert not external_home.exists()
|
||||
@ -11,6 +11,7 @@ Exit codes:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
@ -979,4 +980,11 @@ def main() -> int:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Parsed here rather than inside main(): the unit tests call main() in-process
|
||||
# under pytest, whose argv must not reach argparse.
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="python scripts/doctor.py",
|
||||
description=("DeerFlow Health Check (make doctor): checks system requirements, configuration, LLM provider, and optional components, then prints an actionable report."),
|
||||
)
|
||||
parser.parse_args()
|
||||
sys.exit(main())
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user