Major restructure from x-reader fork to independent project: Architecture: - readers/ — content extraction from 10+ platforms (based on x-reader, MIT) - search/ — semantic search via Exa, GitHub API, birdx (NEW) - config.py — configuration management (~/.agent-eyes/config.yaml) (NEW) - doctor.py — environment health checker (NEW) - core.py — AgentEyes unified entry point (NEW) - cli.py — full CLI: read, search, setup, doctor (NEW) - integrations/mcp_server.py — 8 MCP tools (NEW) - guides/ — 6 Agent-readable setup guides (NEW) - integrations/skill/ — OpenClaw Skill package (NEW) Platforms (zero config): - Web pages, GitHub, Bilibili, YouTube, RSS, single tweets Platforms (one free API key): - Web search, Reddit search, Twitter search (via Exa) Platforms (optional setup): - Reddit full reader, Twitter advanced, WeChat, XiaoHongShu Tests: 34/34 passing Credits: Built on x-reader by @runes_leo (MIT License)
142 lines
4.9 KiB
Python
142 lines
4.9 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Tests for Agent Eyes search modules."""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from agent_eyes.config import Config
|
|
|
|
|
|
@pytest.fixture
|
|
def tmp_config(tmp_path):
|
|
c = Config(config_path=tmp_path / "config.yaml")
|
|
c.set("exa_api_key", "test-key")
|
|
return c
|
|
|
|
|
|
class TestExaSearch:
|
|
@patch("agent_eyes.search.exa.requests.post")
|
|
@pytest.mark.asyncio
|
|
async def test_search_web(self, mock_post, tmp_config):
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = {
|
|
"results": [
|
|
{
|
|
"title": "Test Result",
|
|
"url": "https://example.com",
|
|
"text": "This is a test snippet",
|
|
"publishedDate": "2025-01-01",
|
|
"score": 0.95,
|
|
}
|
|
]
|
|
}
|
|
mock_resp.raise_for_status = MagicMock()
|
|
mock_post.return_value = mock_resp
|
|
|
|
from agent_eyes.search.exa import search_web
|
|
results = await search_web("test query", config=tmp_config)
|
|
|
|
assert len(results) == 1
|
|
assert results[0]["title"] == "Test Result"
|
|
assert results[0]["url"] == "https://example.com"
|
|
assert results[0]["snippet"] == "This is a test snippet"
|
|
|
|
def test_search_web_requires_key(self):
|
|
from agent_eyes.search.exa import _get_api_key
|
|
with pytest.raises(ValueError, match="Exa API key"):
|
|
_get_api_key(Config(config_path="/tmp/nonexistent/config.yaml"))
|
|
|
|
|
|
class TestRedditSearch:
|
|
@patch("agent_eyes.search.exa.requests.post")
|
|
@pytest.mark.asyncio
|
|
async def test_search_reddit(self, mock_post, tmp_config):
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = {"results": []}
|
|
mock_resp.raise_for_status = MagicMock()
|
|
mock_post.return_value = mock_resp
|
|
|
|
from agent_eyes.search.reddit import search_reddit
|
|
results = await search_reddit("test", config=tmp_config)
|
|
|
|
# Verify it searched site:reddit.com
|
|
call_args = mock_post.call_args
|
|
query = call_args[1]["json"]["query"]
|
|
assert "site:reddit.com" in query
|
|
|
|
@patch("agent_eyes.search.exa.requests.post")
|
|
@pytest.mark.asyncio
|
|
async def test_search_reddit_with_sub(self, mock_post, tmp_config):
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = {"results": []}
|
|
mock_resp.raise_for_status = MagicMock()
|
|
mock_post.return_value = mock_resp
|
|
|
|
from agent_eyes.search.reddit import search_reddit
|
|
await search_reddit("test", subreddit="LocalLLaMA", config=tmp_config)
|
|
|
|
call_args = mock_post.call_args
|
|
query = call_args[1]["json"]["query"]
|
|
assert "site:reddit.com/r/LocalLLaMA" in query
|
|
|
|
|
|
class TestGitHubSearch:
|
|
@patch("agent_eyes.search.github.requests.get")
|
|
@pytest.mark.asyncio
|
|
async def test_search_github(self, mock_get):
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = {
|
|
"items": [
|
|
{
|
|
"full_name": "owner/repo",
|
|
"html_url": "https://github.com/owner/repo",
|
|
"description": "A test repo",
|
|
"stargazers_count": 100,
|
|
"forks_count": 20,
|
|
"language": "Python",
|
|
"updated_at": "2025-01-01",
|
|
"topics": ["ai"],
|
|
}
|
|
]
|
|
}
|
|
mock_resp.raise_for_status = MagicMock()
|
|
mock_get.return_value = mock_resp
|
|
|
|
from agent_eyes.search.github import search_github
|
|
results = await search_github("test")
|
|
|
|
assert len(results) == 1
|
|
assert results[0]["name"] == "owner/repo"
|
|
assert results[0]["stars"] == 100
|
|
|
|
@patch("agent_eyes.search.github.requests.get")
|
|
@pytest.mark.asyncio
|
|
async def test_search_github_with_language(self, mock_get):
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = {"items": []}
|
|
mock_resp.raise_for_status = MagicMock()
|
|
mock_get.return_value = mock_resp
|
|
|
|
from agent_eyes.search.github import search_github
|
|
await search_github("test", language="python")
|
|
|
|
call_args = mock_get.call_args
|
|
assert "language:python" in call_args[1]["params"]["q"]
|
|
|
|
|
|
class TestTwitterSearch:
|
|
@patch("agent_eyes.search.twitter.shutil.which", return_value=None)
|
|
@patch("agent_eyes.search.exa.requests.post")
|
|
@pytest.mark.asyncio
|
|
async def test_search_twitter_exa_fallback(self, mock_post, mock_which, tmp_config):
|
|
mock_resp = MagicMock()
|
|
mock_resp.json.return_value = {"results": []}
|
|
mock_resp.raise_for_status = MagicMock()
|
|
mock_post.return_value = mock_resp
|
|
|
|
from agent_eyes.search.twitter import search_twitter
|
|
results = await search_twitter("test", config=tmp_config)
|
|
|
|
call_args = mock_post.call_args
|
|
query = call_args[1]["json"]["query"]
|
|
assert "site:x.com" in query
|