diff --git a/backend/tests/test_check_script.py b/backend/tests/test_check_script.py index 56088a8275..a5640365f0 100644 --- a/backend/tests/test_check_script.py +++ b/backend/tests/test_check_script.py @@ -1,6 +1,8 @@ from __future__ import annotations import importlib.util +import os +import shutil import subprocess import sys from pathlib import Path @@ -192,3 +194,54 @@ def test_check_preserves_unicode_pnpm_error(tmp_path, monkeypatch, host_encoding def test_check_replaces_invalid_tool_output_bytes(): script = _load_script(CHECK_SCRIPT_PATH, "check_invalid_tool_bytes") assert script.run_command([sys.executable, "-c", "import sys; sys.stdout.buffer.write(b'v22.0.0\\xff')"]) == "v22.0.0\ufffd" + + +@pytest.fixture +def check_checkout(tmp_path: Path) -> Path: + checkout = tmp_path / "checkout" + scripts_dir = checkout / "scripts" + scripts_dir.mkdir(parents=True) + (checkout / "frontend").mkdir() + for name in ("check.py", "pnpm.py"): + shutil.copyfile(REPO_ROOT / "scripts" / name, scripts_dir / name) + return checkout + + +def _run_check(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" / "check.py"), *args], + cwd=checkout, + env=env, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + check=False, + timeout=120, + ) + + +def test_help_prints_usage_without_running_the_check(check_checkout: Path): + result = _run_check(check_checkout, "--help") + + assert result.returncode == 0, result.stderr + assert "usage:" in result.stdout + assert "Checking Required Dependencies" not in result.stdout + + +def test_unknown_flag_exits_2_with_usage_error(check_checkout: Path): + result = _run_check(check_checkout, "--verbos") + + assert result.returncode == 2 + assert "unrecognized arguments: --verbos" in result.stderr + assert "Checking Required Dependencies" not in result.stdout + + +def test_no_arguments_still_runs_the_check(check_checkout: Path): + result = _run_check(check_checkout) + + # The disposable checkout inherits this host's toolchain, so the probes may + # pass or fail; either exit code must still come with the normal report. + assert result.returncode in (0, 1), result.stderr + assert "Checking Required Dependencies" in result.stdout diff --git a/backend/tests/test_setup_wizard_script.py b/backend/tests/test_setup_wizard_script.py new file mode 100644 index 0000000000..33c0e50495 --- /dev/null +++ b/backend/tests/test_setup_wizard_script.py @@ -0,0 +1,63 @@ +"""CLI contract for scripts/setup_wizard.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 wizard_checkout(tmp_path: Path) -> Path: + checkout = tmp_path / "checkout" + scripts_dir = checkout / "scripts" + scripts_dir.mkdir(parents=True) + shutil.copyfile(REPO_ROOT / "scripts" / "setup_wizard.py", scripts_dir / "setup_wizard.py") + return checkout + + +def _run_wizard(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" / "setup_wizard.py"), *args], + cwd=checkout, + env=env, + stdin=subprocess.DEVNULL, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + check=False, + timeout=30, + ) + + +def test_help_prints_usage_without_starting_the_wizard(wizard_checkout: Path): + result = _run_wizard(wizard_checkout, "--help") + + assert result.returncode == 0, result.stderr + assert "usage:" in result.stdout + assert "Welcome to DeerFlow Setup!" not in result.stdout + assert "Non-interactive environment detected" not in result.stdout + + +def test_unknown_flag_exits_2_with_usage_error(wizard_checkout: Path): + result = _run_wizard(wizard_checkout, "--verbos") + + assert result.returncode == 2 + assert "unrecognized arguments: --verbos" in result.stderr + assert "Welcome to DeerFlow Setup!" not in result.stdout + + +def test_no_arguments_still_enters_the_wizard(wizard_checkout: Path): + result = _run_wizard(wizard_checkout) + + # The subprocess has no tty, so the wizard reports that instead of prompting. + assert result.returncode == 1, result.stderr + assert "Non-interactive environment detected" in result.stdout diff --git a/scripts/check.py b/scripts/check.py index 74b5963f21..640b7501c5 100644 --- a/scripts/check.py +++ b/scripts/check.py @@ -3,6 +3,7 @@ from __future__ import annotations +import argparse import os import shutil import subprocess @@ -194,4 +195,15 @@ def main() -> int: if __name__ == "__main__": + # Parsed here rather than inside main(): the dependency-check tests call + # main() in-process under pytest, whose argv must not reach argparse. + parser = argparse.ArgumentParser( + prog="python scripts/check.py", + description=( + "DeerFlow dependency check (make check): probes the host tools the " + "local development setup needs and prints install hints for the " + "missing ones. Takes no arguments." + ), + ) + parser.parse_args() sys.exit(main()) diff --git a/scripts/setup_wizard.py b/scripts/setup_wizard.py index 077cc61fcb..6aec09363a 100644 --- a/scripts/setup_wizard.py +++ b/scripts/setup_wizard.py @@ -7,6 +7,7 @@ Usage: from __future__ import annotations +import argparse import sys from pathlib import Path @@ -174,4 +175,17 @@ def main() -> int: if __name__ == "__main__": + # Parsed here rather than inside main(): the wizard tests call main() + # in-process, whose argv must not reach argparse. It also keeps --help from + # entering the prompts, which on an existing config.yaml leads to the + # "reconfigure?" question and rewrites the operator's configuration. + parser = argparse.ArgumentParser( + prog="python scripts/setup_wizard.py", + description=( + "DeerFlow interactive setup wizard (make setup): prompts for the LLM " + "provider, web search/fetch and sandbox settings, then writes " + "config.yaml and .env. Takes no arguments." + ), + ) + parser.parse_args() sys.exit(main())