ForcePilot/backend/test/unit/channels/test_urbit_adapter_supplement.py

1816 lines
60 KiB
Python
Raw Normal View History

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