Files
graphify/tests/test_backend_env_isolation.py
N. Hariharan 13294c0cf0 feat(llm): opt-in backend selection, disable switch, and honest auto-detect (#4361)
detect_backend() picks a cloud provider purely from whichever API key is set
in the environment, and community labeling additionally falls back to claude -p
when the CLI is on PATH. Keys in a developer environment are often there for
other tools, so a bare `graphify extract .` could send uncached documents to a
provider the user did not intend.

Add explicit control without breaking existing auto-detect flows:

- GRAPHIFY_BACKEND=<name> selects a backend explicitly and wins over ambient
  keys; GRAPHIFY_BACKEND=none disables every LLM backend, including the label
  claude -p fallback. `graphify extract` then keeps the local AST pass and
  reports what it held back instead of failing or auto-selecting a provider.
- Auto-detection now reports the exact environment variable that selected the
  backend, so sending a corpus to a cloud provider is never a surprise.
- The claude -p labeling fallback is announced, and GRAPHIFY_BACKEND=none opts
  out of it (it is otherwise kept: silently downgrading good labels to
  "Community N" was the reason it exists).
- extract_files_direct raises a clear error naming GRAPHIFY_BACKEND when the
  library caller would otherwise auto-detect a disabled backend.

README data-residency note updated. detect_backend_with_source() now carries the
detection logic and names its source; detect_backend() delegates to it.

Note: this deliberately keeps auto-detection ON by default (the issue's
alternative #2) rather than flipping cloud use to opt-in, which would break
every existing key-less `graphify extract` flow. GRAPHIFY_BACKEND makes the
opt-in switch a one-liner for a follow-up if maintainers prefer it.

(cherry picked from commit 7e2467ea43)
2026-10-16 17:10:31 +01:00

52 lines
2.1 KiB
Python

"""Backend-detection tests must not depend on the developer's shell (#3481).
`detect_backend()` reads a dozen environment variables. The autouse fixture in
conftest.py clears all of them; these tests keep that list complete and prove
the fixture makes the detection tests immune to ambient keys.
"""
import inspect
import os
import re
import subprocess
import sys
from pathlib import Path
from graphify import llm
ROOT = Path(__file__).resolve().parent.parent
def test_env_list_covers_every_variable_detect_backend_reads():
source = (
inspect.getsource(llm.detect_backend_with_source)
+ inspect.getsource(llm._resolve_ollama_base_url)
)
direct = set(re.findall(r'os\.environ\.get\("([A-Z_]+)"', source))
builtin_keys = {key for name in llm.BACKENDS for key in llm._backend_env_keys(name)}
listed = set(llm.backend_detection_env_vars())
assert direct <= listed, sorted(direct - listed)
assert builtin_keys <= listed, sorted(builtin_keys - listed)
# The regression trigger from the report and the two Azure halves are in there.
assert {"GOOGLE_API_KEY", "GEMINI_API_KEY", "AZURE_OPENAI_ENDPOINT", "OLLAMA_HOST"} <= listed
def test_autouse_fixture_clears_the_backend_environment():
for key in llm.backend_detection_env_vars():
assert key not in os.environ, key
assert llm.detect_backend() is None
def test_detection_tests_pass_with_every_backend_variable_exported():
"""The report's repro: run the detection tests with ambient keys set."""
env = {**os.environ, **{key: "ambient-dummy" for key in llm.backend_detection_env_vars()}}
env["AZURE_OPENAI_ENDPOINT"] = "https://example.openai.azure.com"
env["OLLAMA_BASE_URL"] = "http://localhost:11434/v1"
env["OLLAMA_HOST"] = "http://localhost:11434"
result = subprocess.run(
[sys.executable, "-m", "pytest", "-q", "-p", "no:cacheprovider",
"tests/test_ollama.py", "tests/test_provider_registry.py", "tests/test_llm_backends.py",
"-k", "detect_backend"],
cwd=ROOT, env=env, capture_output=True, text=True, timeout=600,
)
assert result.returncode == 0, result.stdout[-3000:] + result.stderr[-1000:]