feat(brain): add Gemini CLI OAuth path for STREAM_BROWSER=false real-time search
Adds a GEMINI_AUTH=oauth (default) sub-mode that shells out to the Gemini CLI using the user's Google-account login instead of an API key. gemini_cli_search() runs `gemini -p <query> -o json --skip-trust --approval-mode yolo`, strips GEMINI_API_KEY/GOOGLE_API_KEY and sets GOOGLE_GENAI_USE_GCA=true so the CLI selects the account OAuth method and fails fast when no login exists. Bounded by a 30s timeout and fail-open to the DDG/Brave/Wikipedia cascade on any failure (CLI missing, login expired, quota 429, timeout). GEMINI_AUTH=apikey keeps the legacy REST path. Specs and docs/llm_contexts.md updated; behaviour covered by tests/test_realtime_gemini_cli.py. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
91
tests/test_realtime_gemini_cli.py
Normal file
91
tests/test_realtime_gemini_cli.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""Behaviour tests for the Gemini CLI (OAuth login) real-time search backend.
|
||||
|
||||
These verify the observable contract of ``gemini_cli_search``:
|
||||
- a fenced answer when the CLI returns a JSON ``response``,
|
||||
- fail-open (``None``) when the CLI is missing, times out, errors, or returns
|
||||
empty/garbage output,
|
||||
- the child process never inherits ``GEMINI_API_KEY`` / ``GOOGLE_API_KEY`` so
|
||||
the CLI uses the account OAuth login rather than API-key auth.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import types
|
||||
|
||||
import jarvis.tools.builtin.realtime_search as rs
|
||||
|
||||
|
||||
def _fake_proc(stdout: str) -> types.SimpleNamespace:
|
||||
return types.SimpleNamespace(stdout=stdout, stderr="", returncode=0)
|
||||
|
||||
|
||||
def test_returns_fenced_answer_on_success(monkeypatch):
|
||||
monkeypatch.setattr(rs, "_gemini_bin", lambda: "/usr/bin/gemini")
|
||||
monkeypatch.setattr(
|
||||
rs.subprocess,
|
||||
"run",
|
||||
lambda *a, **k: _fake_proc('{"response": "Seoul is sunny, 21C.", "stats": {}}'),
|
||||
)
|
||||
|
||||
out = rs.gemini_cli_search("weather in Seoul")
|
||||
|
||||
assert out is not None
|
||||
assert "Seoul is sunny, 21C." in out
|
||||
assert "UNTRUSTED WEB EXTRACT" in out # untrusted-data fence preserved
|
||||
|
||||
|
||||
def test_none_when_cli_not_installed(monkeypatch):
|
||||
monkeypatch.setattr(rs, "_gemini_bin", lambda: None)
|
||||
# subprocess.run must not even be reached
|
||||
monkeypatch.setattr(rs.subprocess, "run", lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not run")))
|
||||
|
||||
assert rs.gemini_cli_search("anything") is None
|
||||
|
||||
|
||||
def test_none_on_empty_query(monkeypatch):
|
||||
monkeypatch.setattr(rs, "_gemini_bin", lambda: "/usr/bin/gemini")
|
||||
assert rs.gemini_cli_search("") is None
|
||||
|
||||
|
||||
def test_none_on_empty_response(monkeypatch):
|
||||
monkeypatch.setattr(rs, "_gemini_bin", lambda: "/usr/bin/gemini")
|
||||
monkeypatch.setattr(rs.subprocess, "run", lambda *a, **k: _fake_proc('{"response": "", "stats": {}}'))
|
||||
assert rs.gemini_cli_search("q") is None
|
||||
|
||||
|
||||
def test_none_on_garbage_output(monkeypatch):
|
||||
monkeypatch.setattr(rs, "_gemini_bin", lambda: "/usr/bin/gemini")
|
||||
monkeypatch.setattr(rs.subprocess, "run", lambda *a, **k: _fake_proc("not json at all"))
|
||||
assert rs.gemini_cli_search("q") is None
|
||||
|
||||
|
||||
def test_none_on_timeout(monkeypatch):
|
||||
monkeypatch.setattr(rs, "_gemini_bin", lambda: "/usr/bin/gemini")
|
||||
|
||||
def _boom(*a, **k):
|
||||
raise subprocess.TimeoutExpired(cmd="gemini", timeout=30)
|
||||
|
||||
monkeypatch.setattr(rs.subprocess, "run", _boom)
|
||||
assert rs.gemini_cli_search("q") is None
|
||||
|
||||
|
||||
def test_api_key_stripped_from_child_env(monkeypatch):
|
||||
monkeypatch.setattr(rs, "_gemini_bin", lambda: "/usr/bin/gemini")
|
||||
monkeypatch.setenv("GEMINI_API_KEY", "AIza-secret")
|
||||
monkeypatch.setenv("GOOGLE_API_KEY", "another-secret")
|
||||
captured = {}
|
||||
|
||||
def _capture(*a, **k):
|
||||
captured["env"] = k.get("env", {})
|
||||
captured["cmd"] = a[0] if a else k.get("args")
|
||||
return _fake_proc('{"response": "ok"}')
|
||||
|
||||
monkeypatch.setattr(rs.subprocess, "run", _capture)
|
||||
rs.gemini_cli_search("q")
|
||||
|
||||
assert "GEMINI_API_KEY" not in captured["env"]
|
||||
assert "GOOGLE_API_KEY" not in captured["env"]
|
||||
# Google-account (OAuth) auth method is selected explicitly so a missing
|
||||
# login fails fast non-interactively instead of erroring on "no auth method".
|
||||
assert captured["env"].get("GOOGLE_GENAI_USE_GCA") == "true"
|
||||
# invoked headless with JSON output
|
||||
assert "-p" in captured["cmd"] and "-o" in captured["cmd"] and "json" in captured["cmd"]
|
||||
Reference in New Issue
Block a user