Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

263 lines
9.6 KiB
Python

"""Tests for the Pi CLI adapter (``pi -p`` non-interactive print mode).
Unit tests mock the version probe, ``shutil.which``, and the credential file so
they run fully offline. The final ``live_llm`` test exercises a real ``pi``
binary against a real Gemini model and self-skips unless both are available.
"""
from __future__ import annotations
import os
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from integrations.llm_cli.pi_cli import PiAdapter
_PROBE = "integrations.llm_cli.pi_cli.run_version_probe"
_WHICH = "integrations.llm_cli.binary_resolver.shutil.which"
_PI_FALLBACK_PATHS = "integrations.llm_cli.pi_cli._fallback_pi_paths"
# --------------------------------------------------------------------------- #
# detect()
# --------------------------------------------------------------------------- #
@patch(_PROBE, return_value=("pi 0.5.0", None))
@patch(_WHICH, return_value="/usr/bin/pi")
def test_detect_logged_in_via_provider_env_key(
_mock_which: MagicMock, _mock_probe: MagicMock
) -> None:
with patch.dict(os.environ, {"GEMINI_API_KEY": " test-key "}, clear=True):
probe = PiAdapter().detect()
assert probe.installed is True
assert probe.logged_in is True
assert probe.bin_path == "/usr/bin/pi"
assert probe.version == "0.5.0"
assert "GEMINI_API_KEY" in probe.detail
@patch(_PROBE, return_value=("pi 0.5.0", None))
@patch(_WHICH, return_value="/usr/bin/pi")
def test_detect_logged_in_via_auth_json(
_mock_which: MagicMock, _mock_probe: MagicMock, tmp_path: Path
) -> None:
agent_dir = tmp_path / "agent"
agent_dir.mkdir()
(agent_dir / "auth.json").write_text(
'{"anthropic": {"type": "oauth", "access": "tok"}}', encoding="utf-8"
)
with patch.dict(os.environ, {"PI_AGENT_DIR": str(agent_dir)}, clear=True):
probe = PiAdapter().detect()
assert probe.installed is True
assert probe.logged_in is True
assert "auth.json" in probe.detail
@patch(_PROBE, return_value=("pi 0.5.0", None))
@patch(_WHICH, return_value="/usr/bin/pi")
def test_detect_not_logged_in_when_no_key_and_no_auth_file(
_mock_which: MagicMock, _mock_probe: MagicMock, tmp_path: Path
) -> None:
with patch.dict(os.environ, {"PI_AGENT_DIR": str(tmp_path)}, clear=True):
probe = PiAdapter().detect()
assert probe.installed is True
assert probe.logged_in is False
assert "/login" in probe.detail or "API key" in probe.detail
@patch(_PROBE, return_value=("pi 0.5.0", None))
@patch(_WHICH, return_value="/usr/bin/pi")
def test_detect_unreadable_auth_json_returns_none(
_mock_which: MagicMock, _mock_probe: MagicMock, tmp_path: Path
) -> None:
(tmp_path / "auth.json").write_text("not-json{", encoding="utf-8")
with patch.dict(os.environ, {"PI_AGENT_DIR": str(tmp_path)}, clear=True):
probe = PiAdapter().detect()
assert probe.installed is True
assert probe.logged_in is None # unclear, invocation will verify
@patch(_PROBE, return_value=("pi 0.5.0", None))
@patch(_WHICH, return_value="/usr/bin/pi")
def test_detect_empty_auth_json_not_logged_in(
_mock_which: MagicMock, _mock_probe: MagicMock, tmp_path: Path
) -> None:
(tmp_path / "auth.json").write_text("{}", encoding="utf-8")
with patch.dict(os.environ, {"PI_AGENT_DIR": str(tmp_path)}, clear=True):
probe = PiAdapter().detect()
assert probe.installed is True
assert probe.logged_in is False
@patch(_PROBE, return_value=(None, "`/usr/bin/pi --version` failed: boom"))
@patch(_WHICH, return_value="/usr/bin/pi")
def test_detect_version_probe_failure_marks_not_installed(
_mock_which: MagicMock, _mock_probe: MagicMock
) -> None:
probe = PiAdapter().detect()
assert probe.installed is False
assert probe.logged_in is None
assert "boom" in probe.detail
@patch(_PI_FALLBACK_PATHS, return_value=[])
@patch(_WHICH, return_value=None)
def test_detect_binary_missing(_mock_which: MagicMock, _mock_fallbacks: MagicMock) -> None:
with patch.dict(os.environ, {}, clear=True):
probe = PiAdapter().detect()
assert probe.installed is False
assert probe.bin_path is None
assert "Pi CLI not found" in probe.detail
# --------------------------------------------------------------------------- #
# build()
# --------------------------------------------------------------------------- #
@patch(_WHICH, return_value="/usr/bin/pi")
def test_build_print_mode_and_model_flag(_mock_which: MagicMock) -> None:
inv = PiAdapter().build(prompt="hello", model="google/gemini-2.5-flash-lite", workspace="")
assert inv.stdin is None
assert inv.argv[0] == "/usr/bin/pi"
assert "-p" in inv.argv
assert "hello" in inv.argv
assert "--model" in inv.argv
idx = inv.argv.index("--model")
assert inv.argv[idx + 1] == "google/gemini-2.5-flash-lite"
assert inv.env is not None and inv.env.get("NO_COLOR") == "1"
assert inv.cwd == os.getcwd()
@patch(_WHICH, return_value="/usr/bin/pi")
def test_build_omits_model_when_empty(_mock_which: MagicMock) -> None:
inv = PiAdapter().build(prompt="p", model="", workspace="")
assert "--model" not in inv.argv
@patch(_WHICH, return_value="/usr/bin/pi")
def test_build_forwards_provider_api_key(_mock_which: MagicMock) -> None:
with patch.dict(
os.environ,
{"GEMINI_API_KEY": "sk-gemini", "SOME_UNRELATED_SECRET": "nope"},
clear=True,
):
inv = PiAdapter().build(prompt="p", model="google/gemini-2.5-flash-lite", workspace="")
assert inv.env is not None
assert inv.env["GEMINI_API_KEY"] == "sk-gemini"
assert "SOME_UNRELATED_SECRET" not in inv.env
@patch(_PI_FALLBACK_PATHS, return_value=[])
@patch(_WHICH, return_value=None)
def test_build_raises_when_binary_missing(
_mock_which: MagicMock, _mock_fallbacks: MagicMock
) -> None:
with (
patch.dict(os.environ, {}, clear=True),
pytest.raises(RuntimeError, match="Pi CLI not found"),
):
PiAdapter().build(prompt="p", model=None, workspace="")
# --------------------------------------------------------------------------- #
# registry / parse / explain_failure
# --------------------------------------------------------------------------- #
def test_pi_registry_entry() -> None:
from integrations.llm_cli.registry import get_cli_provider_registration
reg = get_cli_provider_registration("pi")
assert reg is not None
assert reg.model_env_key == "PI_MODEL"
assert reg.adapter_factory().name == "pi"
def test_parse_strips_and_raises_on_empty() -> None:
adapter = PiAdapter()
assert adapter.parse(stdout=" pong \n", stderr="", returncode=0) == "pong"
with pytest.raises(RuntimeError, match="empty output"):
adapter.parse(stdout=" ", stderr="", returncode=0)
def test_explain_failure_classifies_messages() -> None:
adapter = PiAdapter()
auth = adapter.explain_failure(stdout="", stderr="Error: not logged in", returncode=1)
assert "Authentication failed" in auth
model = adapter.explain_failure(stdout="", stderr="model not found: foo", returncode=1)
assert "PI_MODEL format" in model
quota = adapter.explain_failure(stdout="", stderr="rate limit exceeded", returncode=1)
assert "Rate limited" in quota
# --------------------------------------------------------------------------- #
# runner integration (real adapter, mocked subprocess)
# --------------------------------------------------------------------------- #
@patch("integrations.llm_cli.runner.subprocess.run")
@patch(_PROBE, return_value=("pi 0.5.0", None))
@patch(_WHICH, return_value="/usr/bin/pi")
def test_cli_backed_client_invoke_forwards_pi_env(
_mock_which: MagicMock, _mock_probe: MagicMock, mock_run: MagicMock
) -> None:
from integrations.llm_cli.runner import CLIBackedLLMClient
mock_run.return_value = MagicMock(returncode=0, stdout="answer\n", stderr="")
with (
patch("platform.guardrails.engine.get_guardrail_engine") as gr,
patch.dict(os.environ, {"GEMINI_API_KEY": "sk-gemini"}, clear=False),
):
gr.return_value.is_active = False
client = CLIBackedLLMClient(
PiAdapter(), model="google/gemini-2.5-flash-lite", max_tokens=256
)
resp = client.invoke("hello")
assert resp.content == "answer"
env = mock_run.call_args.kwargs["env"]
assert env["GEMINI_API_KEY"] == "sk-gemini"
assert env["NO_COLOR"] == "1"
# --------------------------------------------------------------------------- #
# live model test (real pi + real Gemini) — self-skips without creds/binary
# --------------------------------------------------------------------------- #
def _require_live_pi_gemini() -> str:
"""Skip unless the ``pi`` binary is installed and a Gemini key is present."""
import shutil
binary = shutil.which("pi") or os.environ.get("PI_BIN", "").strip()
if not binary:
pytest.skip("pi binary not installed; skipping live Pi test")
if not os.environ.get("GEMINI_API_KEY", "").strip():
pytest.skip("GEMINI_API_KEY not set; skipping live Pi test")
return os.environ.get("PI_MODEL", "").strip() or "google/gemini-2.5-flash-lite"
@pytest.mark.integration
@pytest.mark.live_llm
def test_live_pi_gemini_round_trip() -> None:
model = _require_live_pi_gemini()
from integrations.llm_cli.runner import CLIBackedLLMClient
adapter = PiAdapter()
probe = adapter.detect()
assert probe.installed is True, probe.detail
assert probe.logged_in is not False, f"Pi reports not authenticated: {probe.detail}"
client = CLIBackedLLMClient(adapter, model=model, max_tokens=256)
resp = client.invoke("Reply with exactly one word: pong")
assert resp.content.strip(), "Pi returned an empty response"
assert "pong" in resp.content.lower()