ForcePilot/backend/test/unit/channels/test_urbit_adapter_supplement.py
Kris 69fe97a90d test: 批量修复并新增单元测试用例
1. 移除Telegram格式化测试中未使用的导入项
2. 修复Teams测试用例,添加monkeypatch参数并配置通配符开关
3. 更新钉钉适配器测试,替换弃用的流属性检查
4. 修正Twitch规范化测试,更新ROOMSTATE测试逻辑
5. 重构会话映射测试,完善数据库执行结果模拟
6. 格式化Slack块构建测试的长参数调用
7. 修复LINE适配器测试,更新能力断言和异步锁使用
8. 修正Slack会话解析测试,修复聊天类型判断错误
9. 更新能力测试,补充缺失的字段检查
10. 修复Matrix适配器测试,修正位置参数和配置校验逻辑
11. 为飞书分析模块测试添加跳过标记
12. 新增微信能力、限流、链接格式、会话路由等模块的单元测试
13. 修复Twitch适配器导入路径和测试断言
14. 新增Discord Webhook、Nextcloud Talk、Signal多账户等模块的单元测试
15. 修复Manager阶段测试的导入路径
16. 新增iMessage异常和命令处理的单元测试
17. 新增Nostr健康检查和相关模块的单元测试
18. 新增Signal守护进程和SSE重连相关测试
2026-05-13 16:43:01 +08:00

1816 lines
60 KiB
Python

from __future__ import annotations
import asyncio
import json
import pytest
from yuxi.channels.adapters.urbit.cites import cite_to_text, parse_cite
from yuxi.channels.adapters.urbit.errors import (
UrbitAuthError,
UrbitError,
UrbitHttpError,
UrbitUrlError,
)
from yuxi.channels.adapters.urbit.format import (
build_media_content,
is_valid_ship,
)
from yuxi.channels.adapters.urbit.history import MessageCache
from yuxi.channels.adapters.urbit.invites import InviteManager
from yuxi.channels.adapters.urbit.monitor import (
_detect_bot_mentions,
_extract_cites_from_message,
_is_self_message,
)
from yuxi.channels.adapters.urbit.probe import (
fetch_with_ssrf_guard,
validate_urbit_url,
)
from yuxi.channels.adapters.urbit.session import (
cleanup_expired_sessions,
clear_unsafe_sessions,
detect_unsafe_session,
get_unsafe_sessions,
)
from yuxi.channels.adapters.urbit.settings import SettingsStore
from yuxi.channels.adapters.urbit.story import (
markdown_to_story,
story_to_plain_text,
)
from yuxi.channels.adapters.urbit.summarizer import Summarizer
from yuxi.channels.adapters.urbit.targets import (
format_target_hint,
parse_tlon_target,
)
from yuxi.channels.adapters.urbit.threads import ThreadManager
from yuxi.channels.adapters.urbit.upload import (
assert_safe_upload_result_url,
assert_trusted_upload_url,
sanitize_filename,
)
from yuxi.channels.adapters.urbit.vision import build_vision_context
from yuxi.channels.models import (
ChannelIdentity,
ChannelMessage,
ChannelType,
ChatType,
MentionsInfo,
)
# =============================================================================
# errors.py
# =============================================================================
class TestUrbitErrors:
def test_urbit_error_base(self):
err = UrbitError("test error")
assert str(err) == "test error"
assert isinstance(err, Exception)
def test_urbit_url_error(self):
err = UrbitUrlError("bad url")
assert str(err) == "bad url"
assert isinstance(err, UrbitError)
def test_urbit_http_error_with_message(self):
err = UrbitHttpError(500, "Server error")
assert err.status_code == 500
assert str(err) == "Server error"
assert isinstance(err, UrbitError)
def test_urbit_http_error_default_message(self):
err = UrbitHttpError(404)
assert err.status_code == 404
assert "404" in str(err)
assert isinstance(err, UrbitError)
def test_urbit_auth_error(self):
err = UrbitAuthError()
assert isinstance(err, UrbitError)
# =============================================================================
# cites.py - cite_to_text
# =============================================================================
class TestCiteToText:
def test_chan_cite(self):
result = cite_to_text({"type": "chan", "group": "test", "chan": "mychan"})
assert "test/mychan" in result
assert result.startswith("[channel:")
def test_group_cite(self):
result = cite_to_text({"type": "group", "group": "mygroup"})
assert result == "[group: mygroup]"
def test_desk_cite(self):
result = cite_to_text({"type": "desk", "desk": "mydesk"})
assert result == "[desk: mydesk]"
def test_bait_cite(self):
result = cite_to_text({"type": "bait", "bait": "mybait"})
assert result == "[bait: mybait]"
def test_unknown_cite_type(self):
result = cite_to_text({"type": "unknown"})
assert result == "[unknown reference]"
def test_parse_cite_group(self):
result = parse_cite({"cite": "group", "group": "test"})
assert result is not None
assert result["type"] == "group"
def test_parse_cite_desk(self):
result = parse_cite({"cite": "desk", "desk": "mydesk"})
assert result is not None
assert result["type"] == "desk"
def test_parse_cite_bait(self):
result = parse_cite({"cite": "bait", "bait": "mybait"})
assert result is not None
assert result["type"] == "bait"
# =============================================================================
# format.py - edge cases for build_media_content
# =============================================================================
class TestBuildMediaContentEdgeCases:
def test_video(self):
result = build_media_content("video", "https://video.com/v.mp4")
assert result[0]["video"]["src"] == "https://video.com/v.mp4"
def test_audio(self):
result = build_media_content("audio", "https://audio.com/a.mp3")
assert result[0]["audio"]["src"] == "https://audio.com/a.mp3"
def test_reference(self):
result = build_media_content("reference", "some text")
assert result[0]["reference"]["text"] == "some text"
# =============================================================================
# format.py - is_valid_ship edge cases
# =============================================================================
class TestShipFormatEdgeCases:
def test_is_valid_ship_uppercase(self):
assert is_valid_ship("~ZOD")
def test_is_valid_ship_single_word(self):
assert is_valid_ship("~zod")
def test_is_valid_ship_multi_word(self):
assert is_valid_ship("~sampel-palnet")
def test_is_valid_ship_invalid_short(self):
assert not is_valid_ship("~ab")
def test_is_valid_ship_invalid_numbers(self):
assert not is_valid_ship("~12")
def test_is_valid_ship_invalid_special(self):
assert not is_valid_ship("~zod!")
# =============================================================================
# story.py - edge cases for markdown_to_story and story_to_plain_text
# =============================================================================
class TestMarkdownToStoryEdgeCases:
def test_empty_content(self):
result = markdown_to_story("")
assert result == [{"text": ""}]
def test_code_block_no_lang(self):
result = markdown_to_story("```\ncode\n```")
has_code = any(
isinstance(item, dict)
and "code" in item
and item["code"]["code"] == "code"
for item in result
)
assert has_code
def test_code_block_with_lang(self):
result = markdown_to_story("```python\nprint('hi')\n```")
has_python_code = any(
isinstance(item, dict)
and "code" in item
and item["code"]["lang"] == "python"
for item in result
)
assert has_python_code
def test_header_h1(self):
result = markdown_to_story("# Title")
has_h1 = any(
isinstance(item, dict)
and "header" in item
and item["header"]["tag"] == "h1"
for item in result
)
assert has_h1
def test_header_h2(self):
result = markdown_to_story("## Section")
has_h2 = any(
isinstance(item, dict)
and "header" in item
and item["header"]["tag"] == "h2"
for item in result
)
assert has_h2
def test_header_h3(self):
result = markdown_to_story("### Subsection")
has_h3 = any(
isinstance(item, dict)
and "header" in item
and item["header"]["tag"] == "h3"
for item in result
)
assert has_h3
def test_bold_text(self):
result = markdown_to_story("**bold**")
assert len(result) > 0
def test_italic_text(self):
result = markdown_to_story("*italic*")
assert len(result) > 0
def test_strikethrough_text(self):
result = markdown_to_story("~~strike~~")
assert len(result) > 0
def test_inline_code(self):
result = markdown_to_story("`code`")
assert len(result) > 0
def test_link(self):
result = markdown_to_story("[link](https://example.com)")
assert len(result) > 0
def test_unordered_list(self):
result = markdown_to_story("- item1\n- item2\n**trigger**")
has_listing = any(
isinstance(item, dict) and "listing" in item for item in result
)
assert has_listing
def test_ordered_list(self):
result = markdown_to_story("1. first\n2. second\n**trigger**")
has_listing = any(
isinstance(item, dict) and "listing" in item for item in result
)
assert has_listing
def test_multiline_text(self):
result = markdown_to_story("line1\nline2")
assert isinstance(result, list)
has_break = any(
isinstance(item, dict) and "break" in item for item in result
) or len(result) >= 2
assert has_break
def test_rule_separator(self):
result = markdown_to_story("text\n---\nmore text")
found_rule = False
for item in result:
if isinstance(item, dict) and "block" in item:
if item["block"].get("rule") is None:
found_rule = True
assert found_rule
class TestStoryToPlainText:
def test_simple_text(self):
story = [{"text": "hello"}]
result = story_to_plain_text(story)
assert result == "hello"
def test_bold(self):
story = [{"bold": [{"text": "bold text"}]}]
result = story_to_plain_text(story)
assert result == "bold text"
def test_italic(self):
story = [{"italics": [{"text": "italic text"}]}]
result = story_to_plain_text(story)
assert result == "italic text"
def test_strike(self):
story = [{"strike": [{"text": "strike text"}]}]
result = story_to_plain_text(story)
assert result == "strike text"
def test_inline_code(self):
story = [{"inline-code": "code"}]
result = story_to_plain_text(story)
assert result == "code"
def test_header(self):
story = [{"header": {"content": "Title", "tag": "h1"}}]
result = story_to_plain_text(story)
assert result == "Title"
def test_code_block(self):
story = [{"code": {"code": "print('hi')", "lang": "python"}}]
result = story_to_plain_text(story)
assert "print('hi')" in result
def test_link(self):
story = [{"link": {"content": "click", "href": "https://example.com"}}]
result = story_to_plain_text(story)
assert "click" in result
def test_break(self):
story = [{"text": "a"}, {"break": None}, {"text": "b"}]
result = story_to_plain_text(story)
assert "\n" in result
def test_ship(self):
story = [{"ship": "~zod"}]
result = story_to_plain_text(story)
assert result == "~zod"
def test_listing(self):
story = [
{
"listing": {
"type": "unordered",
"items": ["item1", "item2"],
"contents": [],
}
}
]
result = story_to_plain_text(story)
assert "item1" in result
assert "item2" in result
def test_blockquote(self):
story = [{"blockquote": [{"text": "quoted text"}]}]
result = story_to_plain_text(story)
assert "quoted text" in result
def test_empty_list(self):
result = story_to_plain_text([])
assert result == ""
# =============================================================================
# session.py
# =============================================================================
class TestUnsafeSession:
def test_detect_unsafe_session_two_participants(self):
clear_unsafe_sessions()
identity = ChannelIdentity(
channel_id="urbit",
channel_type=ChannelType.URBIT,
channel_user_id="~zod",
channel_chat_id="dm--marzod",
)
msg = ChannelMessage(
identity=identity,
content="hi",
metadata={"chat_type": "direct", "urbit_ship": "zod"},
)
result = detect_unsafe_session(msg)
assert result == []
def test_detect_unsafe_session_group_returns_empty(self):
clear_unsafe_sessions()
identity = ChannelIdentity(
channel_id="urbit",
channel_type=ChannelType.URBIT,
channel_user_id="~zod",
channel_chat_id="/~zod/my-group/chat/1",
)
msg = ChannelMessage(
identity=identity,
content="hi",
metadata={"chat_type": "group", "urbit_ship": "zod"},
)
result = detect_unsafe_session(msg)
assert result == []
def test_detect_unsafe_session_no_ship(self):
clear_unsafe_sessions()
identity = ChannelIdentity(
channel_id="urbit",
channel_type=ChannelType.URBIT,
channel_user_id="~zod",
channel_chat_id="dm--marzod",
)
msg = ChannelMessage(
identity=identity,
content="hi",
metadata={"chat_type": "direct"},
)
result = detect_unsafe_session(msg)
assert result == []
def test_detect_unsafe_session_many_participants(self):
clear_unsafe_sessions()
identity = ChannelIdentity(
channel_id="urbit",
channel_type=ChannelType.URBIT,
channel_user_id="~zod",
channel_chat_id="dm--marzod",
)
for ship in ["zod", "marzod", "dev"]:
msg = ChannelMessage(
identity=identity,
content="hi",
metadata={"chat_type": "direct", "urbit_ship": ship},
)
detect_unsafe_session(msg)
result = detect_unsafe_session(msg)
assert len(result) > 2
def test_get_unsafe_sessions(self):
clear_unsafe_sessions()
identity = ChannelIdentity(
channel_id="urbit",
channel_type=ChannelType.URBIT,
channel_user_id="~zod",
channel_chat_id="dm--marzod",
)
msg = ChannelMessage(
identity=identity,
content="hi",
metadata={"chat_type": "direct", "urbit_ship": "zod"},
)
detect_unsafe_session(msg)
sessions = get_unsafe_sessions()
assert len(sessions) > 0
def test_clear_unsafe_sessions(self):
identity = ChannelIdentity(
channel_id="urbit",
channel_type=ChannelType.URBIT,
channel_user_id="~zod",
channel_chat_id="dm--marzod",
)
msg = ChannelMessage(
identity=identity,
content="hi",
metadata={"chat_type": "direct", "urbit_ship": "zod"},
)
detect_unsafe_session(msg)
clear_unsafe_sessions()
sessions = get_unsafe_sessions()
assert len(sessions) == 0
def test_cleanup_expired_sessions_returns_int(self):
clear_unsafe_sessions()
count = cleanup_expired_sessions()
assert isinstance(count, int)
# =============================================================================
# authorization.py
# =============================================================================
class TestChannelAuthorization:
def test_is_channel_allowed_open_default(self):
from yuxi.channels.adapters.urbit.authorization import ChannelAuthorization
auth = ChannelAuthorization({})
assert auth.is_channel_allowed("chat/test", "zod") is True
def test_is_channel_allowed_disabled(self):
from yuxi.channels.adapters.urbit.authorization import ChannelAuthorization
config = {
"authorization": {
"channel_rules": {"chat/test": {"policy": "disabled"}}
}
}
auth = ChannelAuthorization(config)
assert auth.is_channel_allowed("chat/test", "zod") is False
def test_is_channel_allowed_allowlist_match(self):
from yuxi.channels.adapters.urbit.authorization import ChannelAuthorization
config = {
"authorization": {
"channel_rules": {
"chat/test": {"policy": "allowlist", "allow_from": ["urbit:zod"]}
}
}
}
auth = ChannelAuthorization(config)
assert auth.is_channel_allowed("chat/test", "zod") is True
assert auth.is_channel_allowed("chat/test", "marzod") is False
def test_is_channel_allowed_default_authorized(self):
from yuxi.channels.adapters.urbit.authorization import ChannelAuthorization
config = {
"authorization": {
"default_authorized_ships": ["urbit:zod"]
}
}
auth = ChannelAuthorization(config)
assert auth.is_channel_allowed("chat/test", "zod") is True
def test_is_channel_allowed_group_allow_from(self):
from yuxi.channels.adapters.urbit.authorization import ChannelAuthorization
config = {"group_allow_from": ["urbit:zod"]}
auth = ChannelAuthorization(config)
assert auth.is_channel_allowed("chat/test", "zod") is True
def test_get_channel_policy(self):
from yuxi.channels.adapters.urbit.authorization import ChannelAuthorization
config = {
"authorization": {
"channel_rules": {"chat/test": {"policy": "allowlist"}}
}
}
auth = ChannelAuthorization(config)
assert auth.get_channel_policy("chat/test") == "allowlist"
assert auth.get_channel_policy("chat/nonexistent") == "open"
def test_add_channel_rule(self):
from yuxi.channels.adapters.urbit.authorization import ChannelAuthorization
auth = ChannelAuthorization({})
auth.add_channel_rule("chat/new", "allowlist", ["urbit:zod"])
assert auth.get_channel_policy("chat/new") == "allowlist"
assert auth.is_channel_allowed("chat/new", "zod") is True
def test_remove_channel_rule(self):
from yuxi.channels.adapters.urbit.authorization import ChannelAuthorization
config = {
"authorization": {
"channel_rules": {"chat/test": {"policy": "disabled"}}
}
}
auth = ChannelAuthorization(config)
auth.remove_channel_rule("chat/test")
assert auth.is_channel_allowed("chat/test", "zod") is True
def test_invalid_policy_returns_false(self):
from yuxi.channels.adapters.urbit.authorization import ChannelAuthorization
config = {
"authorization": {
"channel_rules": {"chat/test": {"policy": "invalid_policy"}}
}
}
auth = ChannelAuthorization(config)
assert auth.is_channel_allowed("chat/test", "zod") is False
# =============================================================================
# accounts.py - AccountInfo and more AccountManager methods
# =============================================================================
class TestAccountInfo:
def test_init(self):
from yuxi.channels.adapters.urbit.accounts import AccountInfo
info = AccountInfo("default", "~zod", "http://localhost:8080", "code123")
assert info.name == "default"
assert info.ship == "zod"
assert info.url == "http://localhost:8080"
assert info.code == "code123"
assert info.status == "disconnected"
assert info.last_probe_at == 0.0
def test_ship_with_tilde(self):
from yuxi.channels.adapters.urbit.accounts import AccountInfo
info = AccountInfo("default", "zod", "http://localhost:8080", "code")
assert info.ship_with_tilde == "~zod"
def test_ship_with_tilde_already_has_tilde(self):
from yuxi.channels.adapters.urbit.accounts import AccountInfo
info = AccountInfo("default", "~zod", "http://localhost:8080", "code")
assert info.ship_with_tilde == "~zod"
def test_to_dict(self):
from yuxi.channels.adapters.urbit.accounts import AccountInfo
info = AccountInfo("default", "zod", "http://localhost:8080", "code")
d = info.to_dict()
assert d["name"] == "default"
assert d["ship"] == "~zod"
assert d["url"] == "http://localhost:8080"
assert d["status"] == "disconnected"
def test_with_metadata(self):
from yuxi.channels.adapters.urbit.accounts import AccountInfo
info = AccountInfo("default", "zod", "http://localhost:8080", "code", {"key": "val"})
assert info.metadata == {"key": "val"}
class TestAccountManagerEdgeCases:
def test_add_account(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
info = mgr.add_account("test", "zod", "http://localhost", "code")
assert info.name == "test"
assert mgr.count == 1
def test_remove_account(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
mgr.add_account("test", "zod", "http://localhost", "code")
assert mgr.remove_account("test") is True
assert mgr.count == 0
def test_remove_nonexistent(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
assert mgr.remove_account("nonexistent") is False
def test_get_default_empty(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
assert mgr.get() is None
def test_get(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
mgr.add_account("test", "zod", "http://localhost", "code")
assert mgr.get("test") is not None
assert mgr.get("test").ship == "zod"
def test_get_all(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
mgr.add_account("a", "zod", "http://localhost", "code")
mgr.add_account("b", "marzod", "http://localhost", "code")
all_accounts = mgr.get_all()
assert len(all_accounts) == 2
def test_list_ships(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
mgr.add_account("a", "zod", "http://localhost", "code")
mgr.add_account("b", "~marzod", "http://localhost", "code")
ships = mgr.list_ships()
assert "~zod" in ships
assert "~marzod" in ships
def test_set_status(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
mgr.add_account("test", "zod", "http://localhost", "code")
mgr.set_status("test", "connected")
assert mgr.get("test").status == "connected"
def test_set_status_nonexistent(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
mgr.set_status("nonexistent", "connected")
def test_is_empty(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
assert mgr.is_empty is True
mgr.add_account("test", "zod", "http://localhost", "code")
assert mgr.is_empty is False
def test_on_status_change(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager()
mgr.add_account("test", "zod", "http://localhost", "code")
calls: list[tuple[str, str]] = []
def handler(name: str, status: str):
calls.append((name, status))
mgr.on_status_change(handler)
mgr.set_status("test", "connected")
assert len(calls) == 1
assert calls[0] == ("test", "connected")
def test_load_from_config(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager([
{"ship": "zod", "url": "http://localhost", "code": "code1"},
{"ship": "marzod", "url": "http://localhost", "code": "code2"},
])
assert mgr.count == 2
def test_load_invalid_skipped(self):
from yuxi.channels.adapters.urbit.accounts import AccountManager
mgr = AccountManager([
{"ship": "", "url": "http://localhost", "code": "x"},
{"ship": "zod", "url": "", "code": "x"},
{"ship": "zod", "url": "http://localhost", "code": ""},
])
assert mgr.count == 0
# =============================================================================
# history.py - MessageCache
# =============================================================================
class TestMessageCache:
@pytest.mark.asyncio
async def test_cache_message(self):
cache = MessageCache()
await cache.cache_message("m1", "chat-1", "hello", "zod")
history = await cache.get_recent("chat-1")
assert len(history) == 1
assert history[0]["content"] == "hello"
@pytest.mark.asyncio
async def test_get_recent_empty(self):
cache = MessageCache()
history = await cache.get_recent("nonexistent")
assert history == []
@pytest.mark.asyncio
async def test_get_channel_history(self):
cache = MessageCache()
await cache.cache_message("m1", "chat-1", "msg1", "zod")
await cache.cache_message("m2", "chat-1", "msg2", "marzod")
history = await cache.get_channel_history("chat-1")
assert len(history) == 2
@pytest.mark.asyncio
async def test_cache_eviction(self):
cache = MessageCache()
for i in range(110):
await cache.cache_message(f"m{i}", "chat-1", f"msg{i}", "zod")
history = await cache.get_recent("chat-1")
assert len(history) <= 100
@pytest.mark.asyncio
async def test_track_sent_message(self):
cache = MessageCache()
await cache.track_sent_message("m1")
assert await cache.is_sent("m1") is True
assert await cache.is_sent("m2") is False
@pytest.mark.asyncio
async def test_update_message(self):
cache = MessageCache()
await cache.cache_message("m1", "chat-1", "original", "zod")
updated = await cache.update_message("m1", "modified")
assert updated is True
history = await cache.get_recent("chat-1")
assert history[0]["content"] == "modified"
@pytest.mark.asyncio
async def test_update_nonexistent(self):
cache = MessageCache()
updated = await cache.update_message("nx", "modified")
assert updated is False
@pytest.mark.asyncio
async def test_clear_channel(self):
cache = MessageCache()
await cache.cache_message("m1", "chat-1", "hello", "zod")
await cache.clear_channel("chat-1")
history = await cache.get_recent("chat-1")
assert history == []
@pytest.mark.asyncio
async def test_get_channel_history_with_limit(self):
cache = MessageCache()
for i in range(10):
await cache.cache_message(f"m{i}", "chat-1", f"msg{i}", "zod")
history = await cache.get_channel_history("chat-1", limit=5)
assert len(history) == 5
@pytest.mark.asyncio
async def test_sent_eviction(self):
cache = MessageCache()
for i in range(210):
await cache.track_sent_message(f"m{i}")
assert await cache.is_sent("m0") is False
# =============================================================================
# summarize.py - more Summarizer edge cases
# =============================================================================
class TestSummarizerEdgeCases:
def test_build_summary_request(self):
cache = MessageCache()
summ = Summarizer(cache)
history = [
{"author": "zod", "content": "hello"},
{"author": "marzod", "content": "hi there"},
]
result = summ.build_summary_request("chat-1", history)
assert result["channel_id"] == "chat-1"
assert result["message_count"] == 2
assert "hello" in result["messages"]
@pytest.mark.asyncio
async def test_get_recent_history_empty(self):
summ = Summarizer()
history = await summ.get_recent_history("nonexistent")
assert history == []
def test_bind_cache(self):
cache = MessageCache()
summ = Summarizer()
summ.bind_cache(cache)
assert summ._message_cache is cache
# =============================================================================
# settings.py - SettingsStore methods
# =============================================================================
class TestSettingsStore:
def _make_client_mock(self):
class MockClient:
ship_name = "zod"
ship_url = "http://localhost"
async def get(self, path, **kwargs):
class MockResp:
status_code = 200
def json(self):
return {}
def raise_for_status(self):
pass
return MockResp()
async def put(self, path, **kwargs):
class MockResp:
status_code = 200
def json(self):
return {}
def raise_for_status(self):
pass
return MockResp()
return MockClient()
def test_get_from_config(self):
store = SettingsStore(self._make_client_mock())
store._file_config = {"key1": "val1"}
assert store.get("key1") == "val1"
def test_get_default(self):
store = SettingsStore(self._make_client_mock())
assert store.get("nonexistent", "default") == "default"
def test_get_from_store(self):
store = SettingsStore(self._make_client_mock())
store._store = {"key1": "store_val"}
store._file_config = {"key1": "config_val"}
assert store.get("key1") == "store_val"
def test_get_all(self):
store = SettingsStore(self._make_client_mock())
store._file_config = {"a": 1}
store._store = {"b": 2}
all_data = store.get_all()
assert all_data["a"] == 1
assert all_data["b"] == 2
def test_merge_key(self):
store = SettingsStore(self._make_client_mock())
result = store.merge_key("key1", "val1")
assert result is True
assert store.get("key1") == "val1"
def test_merge_key_version_conflict(self):
store = SettingsStore(self._make_client_mock())
store.merge_key("key1", "v1")
result = store.merge_key("key1", "v2", expected_version=0)
assert result is False
assert store.get("key1") == "v1"
def test_replace_all(self):
store = SettingsStore(self._make_client_mock())
store.replace_all({"new": "data"})
assert store.get("new") == "data"
def test_update_key(self):
store = SettingsStore(self._make_client_mock())
store.update_key("key1", "val1")
assert store.get("key1") == "val1"
def test_should_migrate_setting_true(self):
store = SettingsStore(self._make_client_mock())
store._file_config = {"key1": "val1"}
assert store.should_migrate_setting("key1") is True
def test_should_migrate_setting_false_already_in_store(self):
store = SettingsStore(self._make_client_mock())
store._file_config = {"key1": "val1"}
store._store = {"key1": "val1"}
assert store.should_migrate_setting("key1") is False
def test_should_migrate_setting_no_file_val(self):
store = SettingsStore(self._make_client_mock())
assert store.should_migrate_setting("nonexistent") is False
def test_is_loaded(self):
store = SettingsStore(self._make_client_mock())
assert store.is_loaded is False
store._loaded = True
assert store.is_loaded is True
def test_version(self):
store = SettingsStore(self._make_client_mock())
assert store.version == 0
store._version = 5
assert store.version == 5
# =============================================================================
# threads.py - ThreadManager
# =============================================================================
class TestThreadManager:
def test_is_thread_reply_true(self):
tm = ThreadManager()
raw = {
"graph-update": {
"additions": {
"1": {"post": {"contents": [{"reply": {"id": "parent-1"}}]}}
}
}
}
assert tm.is_thread_reply(raw) is True
def test_is_thread_reply_false(self):
tm = ThreadManager()
raw = {
"graph-update": {
"additions": {
"1": {"post": {"contents": [{"text": "hello"}]}}
}
}
}
assert tm.is_thread_reply(raw) is False
def test_is_thread_reply_empty(self):
tm = ThreadManager()
assert tm.is_thread_reply({}) is False
def test_get_reply_parent_id(self):
tm = ThreadManager()
raw = {
"graph-update": {
"additions": {
"1": {
"post": {
"contents": [{"reply": {"id": "parent-1"}}]
}
}
}
}
}
parent_id = tm.get_reply_parent_id(raw)
assert parent_id == "parent-1"
def test_get_reply_parent_id_none(self):
tm = ThreadManager()
raw = {
"graph-update": {
"additions": {
"1": {"post": {"contents": [{"text": "hello"}]}}
}
}
}
assert tm.get_reply_parent_id(raw) is None
def test_add_and_has_participated(self):
tm = ThreadManager()
tm.add_participated("thread-1")
assert tm.has_participated("thread-1") is True
assert tm.has_participated("thread-2") is False
def test_build_thread_context(self):
tm = ThreadManager(context_lines=3)
history = [
{"author": "zod", "content": "msg1"},
{"author": "marzod", "content": "msg2"},
{"author": "dev", "content": "msg3"},
{"author": "zod", "content": "msg4"},
]
context = tm.build_thread_context(history)
assert "msg2" in context
assert "msg3" in context
assert "msg4" in context
assert "[Thread conversation" in context
def test_build_thread_context_empty(self):
tm = ThreadManager()
assert tm.build_thread_context([]) == ""
def test_bind_settings_store(self):
tm = ThreadManager()
mock_store = object()
tm.bind_settings_store(mock_store)
assert tm._settings_store is mock_store
def test_init_defaults(self):
tm = ThreadManager()
assert tm._max_history == 20
assert tm._context_lines == 10
@pytest.mark.asyncio
async def test_restore_from_store_empty(self):
tm = ThreadManager()
await tm.restore_from_store()
assert len(tm._participated) == 0
# =============================================================================
# monitor.py - _is_self_message, _detect_bot_mentions, _extract_cites
# =============================================================================
class TestMonitorHelpers:
def _make_msg(self, ship: str, content: str = "hello", additions=None):
identity = ChannelIdentity(
channel_id="urbit",
channel_type=ChannelType.URBIT,
channel_user_id=f"~{ship}",
channel_chat_id="chat/test",
)
metadata = {"urbit_ship": ship}
if additions:
metadata["raw_additions"] = additions
return ChannelMessage(
identity=identity,
content=content,
metadata=metadata,
)
def test_is_self_message_true(self):
msg = self._make_msg("zod")
assert _is_self_message(msg, "zod") is True
def test_is_self_message_false(self):
msg = self._make_msg("marzod")
assert _is_self_message(msg, "zod") is False
def test_is_self_message_no_bot_ship(self):
msg = self._make_msg("zod")
assert _is_self_message(msg, "") is False
def test_is_self_message_case_insensitive(self):
msg = self._make_msg("ZOD")
assert _is_self_message(msg, "zod") is True
def test_detect_bot_mentions_at_all(self):
msg = self._make_msg("marzod", "hello @all")
result = _detect_bot_mentions(msg, "zod")
assert result is not None
assert result.is_bot_mentioned is True
assert "@all" in result.mentioned_user_ids
def test_detect_bot_mentions_at_ship(self):
msg = self._make_msg("marzod", "hello @zod")
result = _detect_bot_mentions(msg, "zod")
assert result is not None
assert result.is_bot_mentioned is True
def test_detect_bot_mentions_with_tilde(self):
msg = self._make_msg("marzod", "hello ~zod")
result = _detect_bot_mentions(msg, "zod")
assert result is not None
assert result.is_bot_mentioned is True
def test_detect_bot_mentions_graph_mention(self):
additions = {
"1": {"post": {"contents": [{"mention": "zod"}]}}
}
msg = self._make_msg("marzod", "hello", additions)
result = _detect_bot_mentions(msg, "zod")
assert result is not None
assert result.is_bot_mentioned is True
def test_detect_bot_mentions_none(self):
msg = self._make_msg("marzod", "hello there")
result = _detect_bot_mentions(msg, "zod")
assert result is None
def test_detect_bot_mentions_no_bot(self):
msg = self._make_msg("marzod", "@zod hi")
result = _detect_bot_mentions(msg, "")
assert result is None
def test_detect_bot_mentions_no_content_none(self):
msg = ChannelMessage(
identity=ChannelIdentity(
channel_id="urbit",
channel_type=ChannelType.URBIT,
channel_user_id="~marzod",
channel_chat_id="chat/test",
),
content="",
metadata={"urbit_ship": "marzod"},
)
result = _detect_bot_mentions(msg, "zod")
assert result is None
def test_extract_cites_from_message(self):
additions = {
"1": {"post": {"contents": [{"cite": "chan", "group": "g", "chan": "c"}]}}
}
msg = ChannelMessage(
identity=ChannelIdentity(
channel_id="urbit",
channel_type=ChannelType.URBIT,
channel_user_id="~zod",
channel_chat_id="chat/test",
),
content="",
metadata={"raw_additions": additions},
)
cites = _extract_cites_from_message(msg)
assert len(cites) == 1
assert cites[0]["type"] == "chan"
def test_extract_cites_empty(self):
msg = ChannelMessage(
identity=ChannelIdentity(
channel_id="urbit",
channel_type=ChannelType.URBIT,
channel_user_id="~zod",
channel_chat_id="chat/test",
),
content="",
metadata={},
)
cites = _extract_cites_from_message(msg)
assert cites == []
# =============================================================================
# probe.py - fetch_with_ssrf_guard
# =============================================================================
class TestSSRFGuard:
def test_fetch_with_ssrf_guard_local_blocked(self):
is_valid, warnings = fetch_with_ssrf_guard("http://127.0.0.1/test")
assert is_valid is False
def test_fetch_with_ssrf_guard_local_allowed(self):
is_valid, _ = fetch_with_ssrf_guard(
"http://127.0.0.1/test", dangerously_allow_private_network=True
)
assert is_valid is True
def test_fetch_with_ssrf_guard_https(self):
is_valid, _ = fetch_with_ssrf_guard("https://example.com/test")
assert is_valid is True
def test_validate_url_empty(self):
is_valid, warnings = validate_urbit_url("")
assert is_valid is False
assert len(warnings) > 0
def test_validate_url_invalid_scheme(self):
is_valid, warnings = validate_urbit_url("ftp://example.com")
assert is_valid is False
def test_validate_url_no_hostname(self):
is_valid, warnings = validate_urbit_url("http://")
assert is_valid is False
# =============================================================================
# upload.py - assert functions
# =============================================================================
class TestUploadAssertions:
def test_assert_trusted_upload_url_valid(self):
assert assert_trusted_upload_url("https://example.com/file.png") is True
def test_assert_trusted_upload_url_invalid(self):
assert assert_trusted_upload_url("http://127.0.0.1/file") is False
def test_assert_safe_upload_result_url_valid(self):
assert assert_safe_upload_result_url("https://example.com/file.png") is True
def test_assert_safe_upload_result_url_invalid(self):
assert assert_safe_upload_result_url("ftp://example.com/file") is False
def test_assert_safe_upload_result_url_no_scheme(self):
assert assert_safe_upload_result_url("just/a/path") is False
# =============================================================================
# vision.py - build_vision_context
# =============================================================================
class TestVision:
@pytest.mark.asyncio
async def test_build_vision_context(self):
result = await build_vision_context(
["https://example.com/img1.png", "https://example.com/img2.png"],
"Describe these images",
)
assert result["source"] == "urbit_vision"
assert len(result["images"]) == 2
assert result["images"][0]["url"] == "https://example.com/img1.png"
@pytest.mark.asyncio
async def test_build_vision_context_default_prompt(self):
result = await build_vision_context(["https://example.com/img.png"])
assert "do you see" in result["prompt"].lower()
@pytest.mark.asyncio
async def test_build_vision_context_empty(self):
result = await build_vision_context([])
assert result["images"] == []
# =============================================================================
# targets.py - parse_tlon_target edge cases
# =============================================================================
class TestTargetFormatEdgeCases:
def test_parse_nest_chat(self):
result = parse_tlon_target("chat/zod/test-group")
assert result["chat_type"] == "group"
assert result["channel_type"] == "chat"
def test_parse_nest_diary(self):
result = parse_tlon_target("diary/zod/test-diary")
assert result["chat_type"] == "group"
assert result["channel_type"] == "diary"
def test_parse_nest_heap(self):
result = parse_tlon_target("heap/zod/test-heap")
assert result["chat_type"] == "group"
assert result["channel_type"] == "heap"
def test_parse_dm_short(self):
result = parse_tlon_target("dm/~zod")
assert result["chat_type"] == "direct"
assert result["peer_id"] == "~zod"
def test_parse_group_with_prefix(self):
result = parse_tlon_target("group:chat/~zod/test-group")
assert result["chat_type"] == "group"
assert "chat" in str(result.get("channel_type", ""))
def test_parse_simple_group(self):
result = parse_tlon_target("group:~zod/test-group")
assert result["chat_type"] == "group"
def test_parse_fallback(self):
result = parse_tlon_target("~unknown-format")
assert result["chat_type"] == "direct"
assert result["peer_id"] == "unknown-format"
def test_format_target_hint_direct_with_tilde(self):
hint = format_target_hint("direct", "~zod", "~host")
assert "dm/~zod" in hint
def test_format_target_hint_group_with_tilde(self):
hint = format_target_hint("group", "test-group", "~host")
assert "host/test-group" in hint
# =============================================================================
# invites.py - InviteManager (non-async methods)
# =============================================================================
class TestInviteManager:
def test_should_auto_accept_group_invite_disabled(self):
mgr = InviteManager(auto_accept_groups=False)
assert mgr.should_auto_accept_group_invite("zod") is False
def test_should_auto_accept_group_invite_empty_allowlist(self):
mgr = InviteManager(auto_accept_groups=True)
assert mgr.should_auto_accept_group_invite("zod") is False
def test_should_auto_accept_group_invite_match(self):
mgr = InviteManager(
auto_accept_groups=True, group_invite_allowlist=["~zod"]
)
assert mgr.should_auto_accept_group_invite("~zod") is True
def test_should_auto_accept_group_invite_no_match(self):
mgr = InviteManager(
auto_accept_groups=True, group_invite_allowlist=["~zod"]
)
assert mgr.should_auto_accept_group_invite("~marzod") is False
def test_should_auto_accept_dm_disabled(self):
mgr = InviteManager(auto_accept_dm=False)
assert mgr.should_auto_accept_dm("zod") is False
def test_should_auto_accept_dm_empty_allowlist(self):
mgr = InviteManager(auto_accept_dm=True)
assert mgr.should_auto_accept_dm("zod") is False
def test_should_auto_accept_dm_match(self):
mgr = InviteManager(auto_accept_dm=True, dm_allowlist=["~zod"])
assert mgr.should_auto_accept_dm("~zod") is True
def test_get_pending_invites_empty(self):
mgr = InviteManager()
assert mgr.get_pending_invites() == []
# =============================================================================
# discovery.py - ChannelDiscovery
# =============================================================================
class TestChannelDiscovery:
def test_init_default(self):
from yuxi.channels.adapters.urbit.discovery import ChannelDiscovery
class MockClient:
ship_name = "zod"
ship_url = "http://localhost"
discovery = ChannelDiscovery(MockClient())
assert discovery._auto_discover is False
assert len(discovery.channels) == 0
def test_init_with_group_channels(self):
from yuxi.channels.adapters.urbit.discovery import ChannelDiscovery
class MockClient:
ship_name = "zod"
ship_url = "http://localhost"
discovery = ChannelDiscovery(
MockClient(), group_channels=["chat/test", "chat/other"]
)
assert "chat/test" in discovery._group_channels
assert "chat/other" in discovery._group_channels
def test_add_channel(self):
from yuxi.channels.adapters.urbit.discovery import ChannelDiscovery
class MockClient:
ship_name = "zod"
ship_url = "http://localhost"
discovery = ChannelDiscovery(MockClient())
discovery.add_channel("chat/test")
assert "chat/test" in discovery.channels
def test_add_channel_duplicate(self):
from yuxi.channels.adapters.urbit.discovery import ChannelDiscovery
class MockClient:
ship_name = "zod"
ship_url = "http://localhost"
discovery = ChannelDiscovery(MockClient())
discovery.add_channel("chat/test")
discovery.add_channel("chat/test")
assert len(discovery.channels) == 1
def test_get_chat_nests(self):
from yuxi.channels.adapters.urbit.discovery import ChannelDiscovery
class MockClient:
ship_name = "zod"
ship_url = "http://localhost"
discovery = ChannelDiscovery(
MockClient(), group_channels=["chat/test-group"]
)
nests = discovery.get_chat_nests("zod")
assert len(nests) == 1
assert "~zod/test-group/chat" in nests[0]
# =============================================================================
# poke_api.py - HttpPokeApiClient
# =============================================================================
class TestHttpPokeApiClient:
def _make_client_mock(self):
class MockClient:
ship_name = "zod"
ship_url = "http://localhost"
async def get(self, path, **kwargs):
class MockResp:
status_code = 200
def json(self):
return {}
def raise_for_status(self):
pass
return MockResp()
async def put(self, path, **kwargs):
class MockResp:
status_code = 200
def json(self):
return {}
def raise_for_status(self):
pass
return MockResp()
async def delete(self, path, **kwargs):
class MockResp:
status_code = 204
def json(self):
return {}
def raise_for_status(self):
pass
return MockResp()
return MockClient()
def test_init(self):
from yuxi.channels.adapters.urbit.poke_api import HttpPokeApiClient
api = HttpPokeApiClient(self._make_client_mock(), "channel-1")
assert api.channel_id == "channel-1"
assert api._closed is False
def test_is_closed_initially_false(self):
from yuxi.channels.adapters.urbit.poke_api import HttpPokeApiClient
api = HttpPokeApiClient(self._make_client_mock(), "channel-1")
assert api.is_closed is False
@pytest.mark.asyncio
async def test_closed_poke_raises(self):
from yuxi.channels.adapters.urbit.poke_api import HttpPokeApiClient
api = HttpPokeApiClient(self._make_client_mock(), "channel-1")
api._closed = True
with pytest.raises(RuntimeError, match="closed"):
await api.poke("chat", "chat-message", {})
@pytest.mark.asyncio
async def test_closed_scry_raises(self):
from yuxi.channels.adapters.urbit.poke_api import HttpPokeApiClient
api = HttpPokeApiClient(self._make_client_mock(), "channel-1")
api._closed = True
with pytest.raises(RuntimeError, match="closed"):
await api.scry("chat", "/path")
@pytest.mark.asyncio
async def test_closed_subscribe_raises(self):
from yuxi.channels.adapters.urbit.poke_api import HttpPokeApiClient
api = HttpPokeApiClient(self._make_client_mock(), "channel-1")
api._closed = True
with pytest.raises(RuntimeError, match="closed"):
await api.subscribe("chat", "/path")
@pytest.mark.asyncio
async def test_close(self):
from yuxi.channels.adapters.urbit.poke_api import HttpPokeApiClient
api = HttpPokeApiClient(self._make_client_mock(), "channel-1")
await api.close()
assert api._closed is True
@pytest.mark.asyncio
async def test_close_idempotent(self):
from yuxi.channels.adapters.urbit.poke_api import HttpPokeApiClient
api = HttpPokeApiClient(self._make_client_mock(), "channel-1")
await api.close()
await api.close()
# =============================================================================
# setup.py
# =============================================================================
class TestSetupSchema:
def test_get_setup_schema(self):
from yuxi.channels.adapters.urbit.setup import get_setup_schema
schema = get_setup_schema()
assert schema["name"] == "urbit"
assert len(schema["required_fields"]) == 3
keys = [f["key"] for f in schema["required_fields"]]
assert "ship_url" in keys
assert "ship_name" in keys
assert "ship_code" in keys
assert len(schema["optional_fields"]) > 0
def test_quick_setup(self):
from yuxi.channels.adapters.urbit.setup import quick_setup
result = quick_setup(
"http://localhost:8080", "zod", "XXXX-XXXX-XXXX-XXXX"
)
assert result["ship_url"] == "http://localhost:8080"
assert result["ship_name"] == "zod"
assert result["ship_code"] == "XXXX-XXXX-XXXX-XXXX"
assert "dm_allowlist" in result
# =============================================================================
# doctor.py - is_legacy more edge cases
# =============================================================================
class TestDoctorEdgeCases:
def test_legacy_allow_private_network_snake(self):
from yuxi.channels.adapters.urbit.doctor import (
is_legacy_private_network_config,
)
assert is_legacy_private_network_config({"allow_private_network": True})
def test_not_legacy(self):
from yuxi.channels.adapters.urbit.doctor import (
is_legacy_private_network_config,
)
assert is_legacy_private_network_config({}) is False
assert is_legacy_private_network_config({"other": True}) is False
def test_create_migrations_empty(self):
from yuxi.channels.adapters.urbit.doctor import (
create_legacy_private_network_doctor_contract,
)
assert create_legacy_private_network_doctor_contract({}) == []
# =============================================================================
# RateLimiter - wait_and_acquire test
# =============================================================================
class TestRateLimiterEdgeCases:
@pytest.mark.asyncio
async def test_wait_and_acquire_success(self):
from yuxi.channels.adapters.urbit.rate_limiter import RateLimiter
limiter = RateLimiter(limit=100, window=60.0)
result = await limiter.wait_and_acquire(timeout=0.5)
assert result is True
@pytest.mark.asyncio
async def test_wait_and_acquire_exhausted_timeout(self):
from yuxi.channels.adapters.urbit.rate_limiter import RateLimiter
limiter = RateLimiter(limit=0, window=60.0)
result = await limiter.wait_and_acquire(timeout=0.1)
assert result is False
# =============================================================================
# UrbitClient - normalize_ship_url
# =============================================================================
class TestNormalizeShipUrl:
def test_strips_trailing_slash(self):
from yuxi.channels.adapters.urbit.client import UrbitClient
client = UrbitClient(ship_url="http://localhost:8080/", ship_name="~zod")
assert client.ship_url == "http://localhost:8080"
def test_adds_https_when_no_scheme(self):
from yuxi.channels.adapters.urbit.client import UrbitClient
client = UrbitClient(ship_url="ship.arvo.network", ship_name="~zod")
assert client.ship_url.startswith("https://")
def test_empty_ship_name(self):
from yuxi.channels.adapters.urbit.client import UrbitClient
client = UrbitClient(ship_url="http://localhost:8080", ship_name="")
assert client.ship_name == ""
assert client.auth_cookie_name == "urbauth-~"
# =============================================================================
# format.py - build_poke_payload with direct chat_type
# =============================================================================
class TestBuildPokePayloadDirectChat:
def test_direct_chat_mark(self):
from yuxi.channels.adapters.urbit.format import build_poke_payload
payload = build_poke_payload(
host_ship="zod",
content="hello",
channel_path="/dm/~marzod",
chat_type="direct",
)
assert payload["mark"] == "chat-dm-action"
# =============================================================================
# __init__.py - exports
# =============================================================================
class TestModuleExports:
def test_all_exports_defined(self):
from yuxi.channels.adapters.urbit import (
AccountManager,
DualPluginRegistry,
HttpPokeApiClient,
InviteManager,
MultiAccountConnectionManager,
UrbitAccountPlugin,
UrbitAdapter,
UrbitSetupPlugin,
create_http_poke_api,
get_categories,
get_config_ui_hints,
get_global_registry,
get_hints_by_category,
with_http_poke_api,
)
assert UrbitAdapter is not None
assert AccountManager is not None
assert HttpPokeApiClient is not None
assert InviteManager is not None
assert DualPluginRegistry is not None
assert get_global_registry is not None
# =============================================================================
# format.py - _extract_graph_content edge cases (diary, blockquote, image, ship)
# =============================================================================
class TestParseGraphUpdateEdgeCases:
def test_parse_diary_with_title(self):
from yuxi.channels.adapters.urbit.format import parse_graph_update
raw = {
"graph-update": {
"resource": {"path": "/~zod/diary-1", "type": "diary"},
"ship": "zod",
"additions": {
"1": {
"post": {
"title": "My Diary Entry",
"contents": [{"text": "body text"}],
}
}
},
}
}
result = parse_graph_update(raw)
assert "My Diary Entry" in result["content"]
assert "body text" in result["content"]
def test_parse_with_image(self):
from yuxi.channels.adapters.urbit.format import parse_graph_update
raw = {
"graph-update": {
"additions": {
"1": {
"post": {
"contents": [
{"image": {"src": "https://img.com/a.png"}}
]
}
}
}
}
}
result = parse_graph_update(raw)
assert "[Image:" in result["content"]
def test_parse_with_ship(self):
from yuxi.channels.adapters.urbit.format import parse_graph_update
raw = {
"graph-update": {
"additions": {
"1": {"post": {"contents": [{"ship": "marzod"}]}}
}
}
}
result = parse_graph_update(raw)
assert "~marzod" in result["content"]
def test_parse_with_blockquote(self):
from yuxi.channels.adapters.urbit.format import parse_graph_update
raw = {
"graph-update": {
"additions": {
"1": {
"post": {
"contents": [
{"blockquote": [{"text": "quoted"}]}
]
}
}
}
}
}
result = parse_graph_update(raw)
assert "quoted" in result["content"]
def test_parse_with_blockquote_string(self):
from yuxi.channels.adapters.urbit.format import parse_graph_update
raw = {
"graph-update": {
"additions": {
"1": {
"post": {
"contents": [
{"blockquote": "simple quote"}
]
}
}
}
}
}
result = parse_graph_update(raw)
assert "simple quote" in result["content"]
# =============================================================================
# summarizer.py - is_summarization_request edge cases
# =============================================================================
class TestSummarizerTriggerEdgeCases:
def test_exact_match_summarise(self):
cache = MessageCache()
summ = Summarizer(cache)
assert summ.is_summarization_request("summarise") is True
def test_case_insensitive(self):
cache = MessageCache()
summ = Summarizer(cache)
assert summ.is_summarization_request("TLDR") is True
def test_leading_whitespace(self):
cache = MessageCache()
summ = Summarizer(cache)
assert summ.is_summarization_request(" summary ") is True
def test_not_summary(self):
cache = MessageCache()
summ = Summarizer(cache)
assert summ.is_summarization_request("how are you today") is False