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