refactor: remove Discord and Toutiao channels (#234)
Discord only returns server metadata (no messages without Bot Token), not useful enough. Toutiao search was fragile (HTML scraping). Both can be revisited when better upstream tools appear. 75 tests passing. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for DiscordChannel."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_reach.channels.discord import DiscordChannel, _get_invite_code
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ch():
|
||||
return DiscordChannel()
|
||||
|
||||
|
||||
class TestDiscordChannelAttributes:
|
||||
def test_name(self, ch):
|
||||
assert ch.name == "discord"
|
||||
|
||||
def test_tier(self, ch):
|
||||
assert ch.tier == 0
|
||||
|
||||
def test_backends(self, ch):
|
||||
assert ch.backends
|
||||
|
||||
|
||||
class TestDiscordCanHandle:
|
||||
def test_discord_gg(self, ch):
|
||||
assert ch.can_handle("https://discord.gg/python")
|
||||
|
||||
def test_discord_com(self, ch):
|
||||
assert ch.can_handle("https://discord.com/invite/rust")
|
||||
|
||||
def test_discord_channel_url(self, ch):
|
||||
assert ch.can_handle("https://discord.com/channels/123/456")
|
||||
|
||||
def test_rejects_other(self, ch):
|
||||
assert not ch.can_handle("https://www.slack.com")
|
||||
|
||||
|
||||
class TestGetInviteCode:
|
||||
def test_discord_gg(self):
|
||||
assert _get_invite_code("https://discord.gg/python") == "python"
|
||||
|
||||
def test_discord_com_invite(self):
|
||||
assert _get_invite_code("https://discord.com/invite/rust-lang") == "rust-lang"
|
||||
|
||||
def test_trailing_slash(self):
|
||||
assert _get_invite_code("https://discord.gg/python/") == "python"
|
||||
|
||||
def test_no_invite_code(self):
|
||||
assert _get_invite_code("https://discord.com/channels/123/456") == ""
|
||||
|
||||
|
||||
class TestDiscordCheck:
|
||||
def test_check_ok_with_exa(self, ch):
|
||||
with patch("agent_reach.channels.discord._exa_available", return_value=True):
|
||||
status, msg = ch.check()
|
||||
assert status == "ok"
|
||||
assert "Exa" in msg
|
||||
|
||||
def test_check_warn_without_exa(self, ch):
|
||||
with patch("agent_reach.channels.discord._exa_available", return_value=False):
|
||||
status, msg = ch.check()
|
||||
assert status == "warn"
|
||||
assert "Invite API" in msg
|
||||
|
||||
|
||||
class TestDiscordRead:
|
||||
def test_read_invite_url(self, ch):
|
||||
mock_data = {
|
||||
"guild": {"name": "Python", "description": "The Python community"},
|
||||
"channel": {"name": "general"},
|
||||
"approximate_member_count": 400000,
|
||||
"approximate_presence_count": 30000,
|
||||
}
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.__enter__ = lambda s: s
|
||||
mock_resp.__exit__ = MagicMock(return_value=False)
|
||||
mock_resp.read.return_value = __import__("json").dumps(mock_data).encode()
|
||||
|
||||
with patch("urllib.request.urlopen", return_value=mock_resp):
|
||||
result = ch.read("https://discord.gg/python")
|
||||
|
||||
assert "Python" in result
|
||||
assert "400,000" in result
|
||||
assert "general" in result
|
||||
@@ -1,117 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for ToutiaoChannel."""
|
||||
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_reach.channels.toutiao import ToutiaoChannel, _parse_search_results
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ch():
|
||||
return ToutiaoChannel()
|
||||
|
||||
|
||||
class TestToutiaoChannelAttributes:
|
||||
def test_name(self, ch):
|
||||
assert ch.name == "toutiao"
|
||||
|
||||
def test_tier(self, ch):
|
||||
assert ch.tier == 0
|
||||
|
||||
def test_backends(self, ch):
|
||||
assert ch.backends
|
||||
|
||||
|
||||
class TestToutiaoCanHandle:
|
||||
def test_matches_toutiao(self, ch):
|
||||
assert ch.can_handle("https://www.toutiao.com/article/123")
|
||||
|
||||
def test_matches_search(self, ch):
|
||||
assert ch.can_handle("https://so.toutiao.com/search?keyword=ai")
|
||||
|
||||
def test_rejects_other(self, ch):
|
||||
assert not ch.can_handle("https://www.baidu.com")
|
||||
|
||||
|
||||
class TestToutiaoCheck:
|
||||
def test_check_ok(self, ch):
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.__enter__ = lambda s: s
|
||||
mock_resp.__exit__ = MagicMock(return_value=False)
|
||||
mock_resp.status = 200
|
||||
mock_resp.read.return_value = b'{"data": {"title": "test"}}'
|
||||
with patch("urllib.request.urlopen", return_value=mock_resp):
|
||||
status, msg = ch.check()
|
||||
assert status == "ok"
|
||||
|
||||
def test_check_exception(self, ch):
|
||||
with patch("urllib.request.urlopen", side_effect=Exception("timeout")):
|
||||
status, msg = ch.check()
|
||||
assert status == "warn"
|
||||
assert "timeout" in msg
|
||||
|
||||
|
||||
class TestParseSearchResults:
|
||||
def _make_script(self, data: dict) -> str:
|
||||
# Production code skips scripts < 1000 chars; pad to exceed threshold
|
||||
payload = json.dumps({"data": data})
|
||||
padding = "x" * max(0, 1001 - len(payload))
|
||||
padded = payload[:-1] + f',"_pad":"{padding}"' + payload[-1]
|
||||
return f'<script type="text/javascript">{padded}</script>'
|
||||
|
||||
def test_extracts_article_url_first(self):
|
||||
html = self._make_script({
|
||||
"title": "测试文章",
|
||||
"article_url": "https://www.toutiao.com/article/1",
|
||||
"template_key": "undefined-default",
|
||||
})
|
||||
results = _parse_search_results(html)
|
||||
assert len(results) == 1
|
||||
assert results[0]["url"] == "https://www.toutiao.com/article/1"
|
||||
assert results[0]["title"] == "测试文章"
|
||||
|
||||
def test_fallback_to_source_url(self):
|
||||
html = self._make_script({
|
||||
"title": "备用 URL 文章",
|
||||
"source_url": "https://www.toutiao.com/source/1",
|
||||
"template_key": "undefined-default",
|
||||
})
|
||||
results = _parse_search_results(html)
|
||||
assert len(results) == 1
|
||||
assert results[0]["url"] == "https://www.toutiao.com/source/1"
|
||||
|
||||
def test_skips_entries_without_url(self):
|
||||
html = self._make_script({
|
||||
"title": "无 URL 文章",
|
||||
"template_key": "undefined-default",
|
||||
})
|
||||
results = _parse_search_results(html)
|
||||
assert results == []
|
||||
|
||||
def test_skips_entries_without_title(self):
|
||||
html = self._make_script({
|
||||
"article_url": "https://www.toutiao.com/article/1",
|
||||
"template_key": "undefined-default",
|
||||
})
|
||||
results = _parse_search_results(html)
|
||||
assert results == []
|
||||
|
||||
def test_skips_skip_templates(self):
|
||||
html = self._make_script({
|
||||
"title": "广告",
|
||||
"article_url": "https://www.toutiao.com/article/1",
|
||||
"template_key": "Bottom-ad",
|
||||
})
|
||||
results = _parse_search_results(html)
|
||||
assert results == []
|
||||
|
||||
def test_empty_html(self):
|
||||
assert _parse_search_results("") == []
|
||||
|
||||
def test_short_scripts_ignored(self):
|
||||
# script < 1000 chars should be skipped
|
||||
html = '<script>{"data": {"title": "x", "article_url": "y"}}</script>'
|
||||
assert _parse_search_results(html) == []
|
||||
Reference in New Issue
Block a user