fix(scripts): make check.py and setup_wizard.py --help print usage (#8152)

Both entry points ignored their arguments entirely, so the first thing a
contributor tries ran the real work: `python scripts/check.py --help` ran the
full dependency probe (and, on a host with a missing tool, exited 1), and
`python scripts/setup_wizard.py --help` entered the wizard -- which on an
existing config.yaml asks "Do you want to reconfigure?" and then rewrites
config.yaml and .env. Unknown flags were silently ignored the same way.

Parse arguments in the __main__ block, following scripts/doctor.py: main()
stays directly callable for the in-process unit tests, --help prints usage and
exits 0 before any probe or prompt, and an unknown flag exits 2 with
"unrecognized arguments". The no-argument invocations behind `make check` and
`make setup` are unchanged.

Adds subprocess CLI-contract cases in a disposable checkout next to the
existing script tests, modelled on backend/tests/test_doctor_script.py.
This commit is contained in:
NanHan Qing 2026-11-12 06:15:52 +08:00 committed by GitHub
parent fc7c7e012c
commit 2baadff446
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 142 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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())

View File

@ -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())