deer-flow/backend/tests/test_configure_script.py
kongxin cee54cdd49
fix(web-fetch): stop sending .env.example placeholder keys to providers (#8206)
* fix(web-fetch): stop sending .env.example placeholder keys to providers

make config and make setup copy .env.example into .env, and the template
set nine search/fetch provider keys to example values such as
JINA_API_KEY=your-jina-api-key. The default keyless Jina Reader sent
that text as a Bearer key, so every web_fetch on a fresh install failed
with 401 while make doctor reported web fetch as configured.

- .env.example comments those nine lines out, like every other credential
  in the template already is.
- provider_credentials.resolve_api_key reads a your-...-key/-token/-secret
  value as unset, so existing .env files that still hold the placeholders
  recover without editing, for every provider that resolves keys there.

* chore: leave the changelog entry to merge time

Per CONTRIBUTING.md (#8202), maintainers add changelog entries when they
merge a pull request.

* fix(web-search): route every provider key and doctor through one placeholder rule

Review follow-up for #8206:

- provider_credentials exposes is_template_placeholder(), the single
  definition of an .env.example example value. key/token/secret must be
  a whole hyphen-separated word, so a real value such as your-monkey or
  your-passkey is no longer dropped.
- Serper, Serply, Sofya, SpecStream, Unbrowse, Webz, Brave, Tencent WSA
  and You.com resolve config and environment keys through
  resolve_api_key instead of private strip-and-send copies, so an old
  .env that still holds the template value no longer sends it.
- make doctor asks the same predicate: a placeholder key is reported as
  still holding the .env.example value instead of "set", and a literal
  placeholder in config.yaml falls back to the environment key as the
  runtime does.
- A census test keeps credential environment reads under community/
  wrapped in resolve_api_key.

* fix(web-search): match shipped placeholders exactly and mirror Browserless precedence in doctor

Second review follow-up for #8206:

- is_template_placeholder() matches exactly the your-... values that
  .env.example ships instead of a pattern, so a token an operator chose
  (a self-hosted Browserless TOKEN such as your-review-team-token) is
  sent again. A test keeps the set equal to the template.
- make doctor mirrors Browserless: a configured token field is
  authoritative, blank or not, and BROWSERLESS_TOKEN is read only when
  the field is omitted. A blank or placeholder token now warns instead
  of reporting the variable, cross-checked against the client.
- The keyless Jina and You.com providers stay ok but name JINA_API_KEY
  or YDC_API_KEY when a placeholder there is ignored, and a literal
  placeholder api_key in config.yaml is named when no env key backs it.

* fix(doctor): match each web provider's key precedence; drop the dead Jina api_key from docs

Found while re-checking #8206 against every provider's runtime:

- Browserless used as web_fetch had no doctor key check; it now gets the
  same authoritative token handling as web_capture.
- InfoQuest reads only INFOQUEST_API_KEY, so doctor no longer counts an
  api_key field in its tool config as configured.
- GroundRoute needs a key for every call but had no doctor entry, so a
  missing key reported ok.
- You.com names a placeholder api_key in config that it ignores.
- The Jina web_fetch docs snippet carried api_key: $JINA_API_KEY, which
  the tool never reads. With JINA_API_KEY now commented out in
  .env.example, that line made config loading fail on a fresh install.
- The provider_credentials docstring now scopes doctor's use of the
  predicate to web provider keys, and the scripts guide no longer says
  .env.example ships live placeholder keys.
2026-11-13 23:23:29 +08:00

113 lines
4.1 KiB
Python

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 config_checkout(tmp_path: Path) -> Path:
scripts_dir = tmp_path / "scripts"
scripts_dir.mkdir()
(tmp_path / "frontend").mkdir()
shutil.copyfile(REPO_ROOT / "scripts" / "configure.py", scripts_dir / "configure.py")
for template in ("config.example.yaml", ".env.example", "frontend/.env.example"):
(tmp_path / template).write_text(f"template: {template}\n", encoding="utf-8")
return tmp_path
@pytest.mark.parametrize("encoding", ["ascii", "cp1252", "cp936", "utf-8"])
def test_configure_succeeds_with_restricted_stdout_encoding(config_checkout: Path, encoding: str):
result = subprocess.run(
[sys.executable, str(config_checkout / "scripts" / "configure.py")],
env={**os.environ, "PYTHONIOENCODING": encoding},
capture_output=True,
text=True,
encoding=encoding,
check=False,
timeout=30,
)
assert result.returncode == 0, result.stderr
assert "Configuration files generated" in result.stdout
assert result.stderr == ""
for template, generated in (
("config.example.yaml", "config.yaml"),
(".env.example", ".env"),
("frontend/.env.example", "frontend/.env"),
):
assert (config_checkout / generated).read_bytes() == (config_checkout / template).read_bytes()
@pytest.mark.parametrize("config_name", ["config.yaml", "config.yml", "configure.yml"])
def test_configure_preserves_existing_configuration(config_checkout: Path, config_name: str):
config_path = config_checkout / config_name
config_path.write_text("existing: true\n", encoding="utf-8")
result = subprocess.run(
[sys.executable, str(config_checkout / "scripts" / "configure.py")],
capture_output=True,
text=True,
encoding="utf-8",
check=False,
timeout=30,
)
assert result.returncode == 1
assert "configuration file already exists" in result.stdout
assert config_path.read_text(encoding="utf-8") == "existing: true\n"
assert not (config_checkout / ".env").exists()
assert not (config_checkout / "frontend" / ".env").exists()
def test_configure_help_prints_usage_without_generating_files(config_checkout: Path):
result = subprocess.run(
[sys.executable, str(config_checkout / "scripts" / "configure.py"), "--help"],
capture_output=True,
text=True,
encoding="utf-8",
check=False,
timeout=30,
)
assert result.returncode == 0, result.stderr
assert "usage: configure.py" in result.stdout
assert "Cross-platform config bootstrap script for DeerFlow." in result.stdout
assert not (config_checkout / "config.yaml").exists()
assert not (config_checkout / ".env").exists()
assert not (config_checkout / "frontend" / ".env").exists()
def test_configure_rejects_unknown_flags_without_generating_files(config_checkout: Path):
result = subprocess.run(
[sys.executable, str(config_checkout / "scripts" / "configure.py"), "--not-a-real-flag"],
capture_output=True,
text=True,
encoding="utf-8",
check=False,
timeout=30,
)
assert result.returncode == 2
assert "unrecognized arguments" in result.stderr
assert not (config_checkout / "config.yaml").exists()
assert not (config_checkout / ".env").exists()
assert not (config_checkout / "frontend" / ".env").exists()
def test_env_template_leaves_every_placeholder_credential_commented():
"""``make config`` and ``make setup`` copy ``.env.example`` verbatim into ``.env``.
A live ``JINA_API_KEY=your-jina-api-key`` line made the default keyless
``web_fetch`` send that text as a key and fail every request with 401, so
every ``your-...`` example value must stay behind a ``#``.
"""
live = [line for line in (REPO_ROOT / ".env.example").read_text(encoding="utf-8").splitlines() if not line.lstrip().startswith("#") and "=" in line and line.split("=", 1)[1].strip().lower().startswith("your-")]
assert live == []