Files
graphify/tests/test_backend_opt_in.py
T
N. Hariharan 7e2467ea43 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.
2026-10-14 09:50:18 +05:30

79 lines
3.0 KiB
Python

"""Explicit backend selection + disable switch for ambient-key auto-detection (#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.
These tests pin the additions that put the user back in control:
- `GRAPHIFY_BACKEND=<name>` selects a backend explicitly, overriding ambient keys;
- `GRAPHIFY_BACKEND=none` disables every LLM backend, including the `claude -p`
fallback, so the AST pass keeps working;
- auto-detection reports the exact environment variable that selected it.
"""
from __future__ import annotations
import pytest
from graphify import llm
DISABLE_SENTINELS = ["none", "off", "disable", "disabled"]
def test_detect_backend_with_source_names_the_ambient_variable(monkeypatch):
monkeypatch.setenv("GEMINI_API_KEY", "ambient")
backend, source = llm.detect_backend_with_source()
assert backend == "gemini"
assert source == "auto-detected from GEMINI_API_KEY"
assert llm.detect_backend() == "gemini"
def test_graphify_backend_selects_explicitly(monkeypatch):
monkeypatch.setenv("GEMINI_API_KEY", "ambient")
monkeypatch.setenv("GRAPHIFY_BACKEND", "openai")
backend, source = llm.detect_backend_with_source()
assert backend == "openai", "GRAPHIFY_BACKEND must win over an ambient key"
assert source == "GRAPHIFY_BACKEND=openai"
@pytest.mark.parametrize("sentinel", DISABLE_SENTINELS)
def test_graphify_backend_none_disables(monkeypatch, sentinel):
monkeypatch.setenv("GEMINI_API_KEY", "ambient")
monkeypatch.setenv("GRAPHIFY_BACKEND", sentinel)
assert llm.detect_backend() is None
assert llm.backend_disabled_by_env() is True
_, source = llm.detect_backend_with_source()
assert source == f"GRAPHIFY_BACKEND={sentinel}"
def test_backend_not_disabled_when_unset(monkeypatch):
assert llm.backend_disabled_by_env() is False
def test_extract_files_direct_refuses_cleanly_when_disabled(monkeypatch):
monkeypatch.setenv("GRAPHIFY_BACKEND", "none")
with pytest.raises(ValueError, match="GRAPHIFY_BACKEND=none"):
llm.extract_files_direct([], backend=None)
def test_generate_community_labels_skips_claude_cli_when_disabled(monkeypatch):
monkeypatch.setenv("GRAPHIFY_BACKEND", "none")
monkeypatch.setattr(llm, "_claude_cli_available", lambda: True)
calls = {"n": 0}
def _spy(*args, **kwargs): # pragma: no cover - must not run
calls["n"] += 1
return {}
monkeypatch.setattr(llm, "label_communities", _spy)
labels, source = llm.generate_community_labels(
None, {0: ["a"], 1: ["b"]}, quiet=True
)
assert source == "placeholder"
assert labels == {0: "Community 0", 1: "Community 1"}
assert calls["n"] == 0, "no backend may be contacted when GRAPHIFY_BACKEND=none"