Files
Agent-Reach/tests/test_cli.py
T
Pnant 380cfd565d feat(identity): capability-layer positioning + skill rewrite, v1.5.0
SKILL.md (per the skill-writing guide):
- description rewritten trigger-first: 调研/全网调研/帮我调研/research lead
  the text (the owner's own phrasing "去全网调研" previously had NO matching
  trigger word — the exact undertrigger the guide warns about); platform
  keywords front-loaded against the 1536-char truncation
- standing rules added: probe `doctor --json` active_backend before acting
  on multi-backend platforms, announce which skill/backend is in use,
  follow the documented retry chains, compose multi-platform research
- quick commands updated to the new reality: bili-cli for bilibili search,
  Reddit/xiaohongshu moved to a "login-backed platforms" section
- SKILL_en.md: same description surgery + the actively harmful stale
  advice removed (anonymous reddit .json curl, yt-dlp for bilibili)

README / README_en:
- tagline gains the capability-layer subline: backends are chosen,
  installed and health-checked for us to swap — the user never notices
- "设计理念" reframed from scaffolding to capability layer (selection /
  install / doctor / routing); channels diagram now shows ordered backend
  lists; selection table split into primary + fallback columns with the
  live-test rationale per row
- platform table: honest rows for bilibili (bili-cli, yt-dlp retired),
  Reddit (no zero-config path), xiaohongshu (OpenCLI / mcp / legacy)

Version 1.5.0 across pyproject / __init__ / CLAUDE.md / test fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 16:30:56 +08:00

186 lines
6.4 KiB
Python

# -*- coding: utf-8 -*-
"""Tests for Agent Reach CLI."""
import shutil
import subprocess
from unittest.mock import patch
import pytest
import requests
import agent_reach.cli as cli
from agent_reach.cli import main
class TestCLI:
def test_version(self, capsys):
with pytest.raises(SystemExit) as exc_info:
with patch("sys.argv", ["agent-reach", "version"]):
main()
assert exc_info.value.code == 0
captured = capsys.readouterr()
assert "Agent Reach v" in captured.out
def test_no_command_shows_help(self, capsys):
with pytest.raises(SystemExit) as exc_info:
with patch("sys.argv", ["agent-reach"]):
main()
assert exc_info.value.code == 0
def test_doctor_runs(self, capsys):
with patch("sys.argv", ["agent-reach", "doctor"]):
main()
captured = capsys.readouterr()
assert "Agent Reach" in captured.out
assert "" in captured.out
def test_transcribe_command_prints_text(self, capsys):
with patch("agent_reach.transcribe.transcribe", return_value="hello transcript"):
with patch("sys.argv", ["agent-reach", "transcribe", "audio.mp3"]):
main()
captured = capsys.readouterr()
assert "hello transcript" in captured.out
def test_transcribe_command_writes_output_file(self, capsys, tmp_path):
out_file = tmp_path / "t.txt"
with patch("agent_reach.transcribe.transcribe", return_value="saved text"):
with patch("sys.argv", ["agent-reach", "transcribe", "audio.mp3", "-o", str(out_file)]):
main()
assert out_file.read_text(encoding="utf-8").strip() == "saved text"
assert "Transcript written" in capsys.readouterr().out
def test_parse_twitter_cookie_input_separate_values(self):
auth_token, ct0 = cli._parse_twitter_cookie_input("token123 ct0abc")
assert auth_token == "token123"
assert ct0 == "ct0abc"
def test_parse_twitter_cookie_input_cookie_header(self):
auth_token, ct0 = cli._parse_twitter_cookie_input(
"auth_token=token123; ct0=ct0abc; other=value"
)
assert auth_token == "token123"
assert ct0 == "ct0abc"
def test_install_rdt_cli_prefers_github_source(self, monkeypatch, capsys):
state = {"rdt_installed": False}
commands = []
def fake_which(name):
if name == "rdt":
return "/usr/local/bin/rdt" if state["rdt_installed"] else None
if name == "pipx":
return "/usr/local/bin/pipx"
return None
def fake_run(cmd, **kwargs):
commands.append(cmd)
state["rdt_installed"] = True
return subprocess.CompletedProcess(cmd, 0, "", "")
monkeypatch.setattr(shutil, "which", fake_which)
monkeypatch.setattr(subprocess, "run", fake_run)
cli._install_rdt_cli()
out = capsys.readouterr().out
assert commands == [["pipx", "install", cli._RDT_GIT_SOURCE]]
assert "✅ rdt-cli installed" in out
def test_install_reddit_deps_routes_by_environment(self, monkeypatch):
"""桌面 → OpenCLI;服务器 → rdt-cli(钉 git 源)。"""
calls = []
monkeypatch.setattr(cli, "_install_opencli_deps", lambda: calls.append("opencli"))
monkeypatch.setattr(cli, "_install_rdt_cli", lambda: calls.append("rdt"))
monkeypatch.setattr(shutil, "which", lambda _: None)
monkeypatch.setattr(cli, "_detect_environment", lambda: "local")
cli._install_reddit_deps()
assert calls == ["opencli"]
calls.clear()
monkeypatch.setattr(cli, "_detect_environment", lambda: "server")
cli._install_reddit_deps()
assert calls == ["rdt"]
class TestCheckUpdateRetry:
def test_retry_timeout_classification(self):
sleeps = []
def fake_sleep(seconds):
sleeps.append(seconds)
with patch("requests.get", side_effect=requests.exceptions.Timeout("timed out")):
resp, err, attempts = cli._github_get_with_retry(
"https://api.github.com/test",
timeout=1,
retries=3,
sleeper=fake_sleep,
)
assert resp is None
assert err == "timeout"
assert attempts == 3
assert sleeps == [1, 2]
def test_retry_dns_classification(self):
error = requests.exceptions.ConnectionError("getaddrinfo failed for api.github.com")
with patch("requests.get", side_effect=error):
resp, err, attempts = cli._github_get_with_retry(
"https://api.github.com/test",
retries=1,
sleeper=lambda _x: None,
)
assert resp is None
assert err == "dns"
assert attempts == 1
def test_retry_rate_limit_then_success(self):
sleeps = []
class R:
def __init__(self, code, payload=None, headers=None):
self.status_code = code
self._payload = payload or {}
self.headers = headers or {}
def json(self):
return self._payload
sequence = [
R(429, headers={"Retry-After": "3"}),
R(200, payload={"tag_name": "v1.5.0"}),
]
with patch("requests.get", side_effect=sequence):
resp, err, attempts = cli._github_get_with_retry(
"https://api.github.com/test",
retries=3,
sleeper=lambda s: sleeps.append(s),
)
assert err is None
assert resp is not None
assert resp.status_code == 200
assert attempts == 2
assert sleeps == [3.0]
def test_classify_rate_limit_from_403(self):
class R:
status_code = 403
headers = {"X-RateLimit-Remaining": "0"}
@staticmethod
def json():
return {"message": "API rate limit exceeded"}
assert cli._classify_github_response_error(R()) == "rate_limit"
def test_check_update_reports_classified_error(self, capsys):
with patch("agent_reach.cli._github_get_with_retry", return_value=(None, "timeout", 3)):
result = cli._cmd_check_update()
captured = capsys.readouterr()
assert result == "error"
assert "网络超时" in captured.out
assert "已重试 3 次" in captured.out