"""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"] # never auto-approve all tools: a search query must not be able to trigger # write/shell tool execution. assert "yolo" not in captured["cmd"] assert "--yolo" not in captured["cmd"]