from __future__ import annotations import pytest from yuxi.channels.adapters.urbit.format import ( build_channel_path, build_chat_action_payload, build_dm_channel_id, build_group_channel_id, build_media_content, build_poke_payload, ensure_ship_tilde, extract_graph_attachments, extract_graph_mentions, is_valid_ship, parse_graph_update, strip_ship_tilde, ) from yuxi.channels.adapters.urbit.monitor import SSEDeduplicator, _graph_update_to_channel_message from yuxi.channels.adapters.urbit.rate_limiter import RateLimiter from yuxi.channels.adapters.urbit.session import resolve_session_route, resolve_thread_key from yuxi.channels.models import ChannelIdentity, ChannelMessage, ChannelType, ChatType class TestShipFormat: def test_is_valid_ship_valid(self): assert is_valid_ship("~zod") assert is_valid_ship("~sampel-palnet") assert is_valid_ship("~marzod") def test_is_valid_ship_invalid(self): assert not is_valid_ship("zod") assert not is_valid_ship("~12") assert not is_valid_ship("~") assert not is_valid_ship("") def test_strip_ship_tilde(self): assert strip_ship_tilde("~zod") == "zod" assert strip_ship_tilde("zod") == "zod" assert strip_ship_tilde("~~marzod") == "marzod" def test_ensure_ship_tilde(self): assert ensure_ship_tilde("~zod") == "~zod" assert ensure_ship_tilde("zod") == "~zod" def test_build_dm_channel_id(self): result = build_dm_channel_id("~zod", "~marzod") assert result == "~zod/dm--marzod" def test_build_group_channel_id(self): result = build_group_channel_id("~zod", "my-group") assert result == "~zod/my-group/chat" class TestChannelPath: def test_build_channel_path_direct(self): path = build_channel_path(chat_type="direct", target_ship="~zod") assert path == "/dm/~zod" def test_build_channel_path_group(self): path = build_channel_path(chat_type="group", group_name="my-group") assert path == "/chat/my-group" def test_build_channel_path_group_with_type(self): path = build_channel_path(chat_type="group", group_name="my-group", channel_type="heap") assert path == "/heap/my-group" class TestParseGraphUpdate: def test_parse_text_message(self): raw = { "graph-update": { "resource": {"path": "/~zod/my-group/chat/1", "type": "chat"}, "ship": "zod", "index": "/1700000000", "time": 1700000000, "additions": { "1": {"post": {"contents": [{"text": "hello world"}]}} }, } } result = parse_graph_update(raw) assert result["content"] == "hello world" assert result["ship"] == "zod" assert result["resource_path"] == "/~zod/my-group/chat/1" def test_parse_with_mention(self): raw = { "graph-update": { "additions": { "1": {"post": {"contents": [{"mention": "marzod"}]}} } } } result = parse_graph_update(raw) assert "~marzod" in result["content"] def test_parse_with_url(self): raw = { "graph-update": { "additions": { "1": {"post": {"contents": [{"url": "https://example.com"}]}} } } } result = parse_graph_update(raw) assert "https://example.com" in result["content"] def test_parse_with_code_block(self): raw = { "graph-update": { "additions": { "1": { "post": { "contents": [ {"code": {"lang": "python", "code": "print('hi')"}} ] } } } } } result = parse_graph_update(raw) assert "```python" in result["content"] assert "print('hi')" in result["content"] def test_parse_with_reaction(self): raw = { "graph-update": { "reaction": {"react": "+1", "ship": "marzod", "msg": "/1700000000"} } } result = parse_graph_update(raw) assert result["reaction"] == {"react": "+1", "ship": "marzod", "msg": "/1700000000"} def test_parse_empty(self): assert parse_graph_update({}) == {} def test_parse_graph_update_not_dict(self): assert parse_graph_update({"graph-update": "invalid"}) == {} assert parse_graph_update({"graph-update": 42}) == {} assert parse_graph_update({"graph-update": []}) == {} def test_extract_graph_mentions(self): additions = { "1": {"post": {"contents": [{"mention": "zod"}, {"text": "hello"}, {"mention": "marzod"}]}} } mentions = extract_graph_mentions(additions) assert mentions == ["zod", "marzod"] def test_extract_graph_mentions_empty(self): assert extract_graph_mentions({}) == [] class TestBuildPayload: def test_build_poke_payload_basic(self): payload = build_poke_payload( host_ship="zod", content="hello", channel_path="/chat/my-group", ) assert payload["action"] == "poke" assert payload["ship"] == "zod" assert payload["app"] == "chat" assert payload["mark"] == "chat-message" assert payload["json"]["path"] == "/chat/my-group" assert payload["json"]["envelope"]["content"] == [{"text": "hello"}] assert payload["json"]["envelope"]["author"] == "~zod" def test_build_poke_payload_with_continuation(self): payload = build_poke_payload( host_ship="zod", content="part2", channel_path="/chat/my-group", continuation=True, ) assert payload["json"]["envelope"]["continuation"] is True def test_build_poke_payload_with_reply_to(self): payload = build_poke_payload( host_ship="zod", content="reply", channel_path="/chat/my-group", reply_to="/1700000000", ) assert payload["json"]["envelope"]["reply_to"] == "/1700000000" def test_build_poke_payload_with_media(self): payload = build_poke_payload( host_ship="zod", content="", channel_path="/chat/my-group", media_content=[{"image": {"src": "https://img.com/a.png"}}], ) assert payload["json"]["envelope"]["content"] == [{"image": {"src": "https://img.com/a.png"}}] def test_build_poke_payload_content_too_long(self): long_content = "x" * 100_001 with pytest.raises(ValueError, match="exceeds maximum"): build_poke_payload( host_ship="zod", content=long_content, channel_path="/chat/my-group", ) def test_build_chat_action_payload_add_react(self): payload = build_chat_action_payload( host_ship="zod", channel_path="/chat/my-group", action="add-react", msg_id="/1700000000", emoji="+1", ) assert payload["mark"] == "chat-action" assert payload["json"]["action"] == {"add-react": {"react": "+1", "msg": "/1700000000"}} def test_build_chat_action_payload_edit(self): payload = build_chat_action_payload( host_ship="zod", channel_path="/chat/my-group", action="edit", msg_id="/1700000000", content="edited text", ) assert payload["json"]["action"]["edit"]["content"] == [{"text": "edited text"}] def test_build_chat_action_payload_del(self): payload = build_chat_action_payload( host_ship="zod", channel_path="/chat/my-group", action="del", msg_id="/1700000000", ) assert payload["json"]["action"] == {"del": {"msg": "/1700000000"}} def test_build_chat_action_payload_unknown_action(self): with pytest.raises(ValueError, match="Unknown chat action"): build_chat_action_payload( host_ship="zod", channel_path="/chat/my-group", action="unknown", msg_id="/1700000000", ) class TestBuildMediaContent: def test_image(self): result = build_media_content("image", "https://img.com/a.png") assert result[0]["image"]["src"] == "https://img.com/a.png" def test_url(self): result = build_media_content("url", "https://example.com") assert result == [{"url": "https://example.com"}] def test_default(self): result = build_media_content("unknown", "hello") assert result == [{"text": "hello"}] class TestSSEDeduplicator: @pytest.mark.asyncio async def test_first_not_duplicate(self): dedup = SSEDeduplicator() assert not await dedup.is_duplicate("index-1") @pytest.mark.asyncio async def test_detect_duplicate(self): dedup = SSEDeduplicator() assert not await dedup.is_duplicate("index-1") assert await dedup.is_duplicate("index-1") @pytest.mark.asyncio async def test_different_indices(self): dedup = SSEDeduplicator() assert not await dedup.is_duplicate("index-1") assert not await dedup.is_duplicate("index-2") @pytest.mark.asyncio async def test_eviction(self): dedup = SSEDeduplicator(max_size=3) for i in range(5): await dedup.is_duplicate(f"index-{i}") assert not await dedup.is_duplicate("index-0") class TestGraphUpdateToChannelMessage: def test_basic_group_message(self): raw = { "graph-update": { "resource": {"path": "/~zod/my-group/chat/1", "type": "chat"}, "ship": "marzod", "index": "/1700000000", "time": 1700000000.0, "additions": { "1": {"post": {"contents": [{"text": "hello"}]}} }, } } msg = _graph_update_to_channel_message(raw, "zod") assert msg.chat_type == ChatType.GROUP assert msg.content == "hello" assert msg.identity.channel_chat_id == "/~zod/my-group/chat/1" assert msg.identity.channel_user_id == "~marzod" assert msg.identity.channel_type == ChannelType.URBIT assert msg.metadata["chat_type"] == "group" def test_direct_message(self): raw = { "graph-update": { "resource": {"path": "/~zod/dm--marzod", "type": "dm"}, "ship": "marzod", "index": "/1700000000", "time": 1700000000.0, "additions": { "1": {"post": {"contents": [{"text": "hi"}]}} }, } } msg = _graph_update_to_channel_message(raw, "zod") assert msg.chat_type == ChatType.DIRECT assert msg.metadata["chat_type"] == "direct" def test_with_mentions(self): raw = { "graph-update": { "resource": {"path": "/~zod/my-group/chat/1", "type": "chat"}, "ship": "marzod", "index": "/1700000000", "time": 1700000000.0, "additions": { "1": {"post": {"contents": [{"mention": "zod"}]}} }, } } msg = _graph_update_to_channel_message(raw, "zod") assert msg.mentions is not None assert "urbit:zod" in msg.mentions.mentioned_user_ids class TestSession: def test_resolve_thread_key_direct(self): identity = ChannelIdentity( channel_id="urbit", channel_type=ChannelType.URBIT, channel_user_id="~marzod", channel_chat_id="dm--zod", ) msg = ChannelMessage( identity=identity, content="hi", metadata={"chat_type": "direct"}, ) result = resolve_thread_key(msg) assert "direct:dm--zod" in result def test_resolve_thread_key_group(self): identity = ChannelIdentity( channel_id="urbit", channel_type=ChannelType.URBIT, channel_user_id="~marzod", channel_chat_id="/~zod/my-group/chat/1", ) msg = ChannelMessage( identity=identity, content="hi", metadata={"chat_type": "group", "group_name": "my-group", "urbit_resource_type": "chat"}, ) result = resolve_thread_key(msg) assert "group:my-group:chat" in result def test_resolve_session_route(self): identity = ChannelIdentity( channel_id="urbit", channel_type=ChannelType.URBIT, channel_user_id="~marzod", channel_chat_id="dm--zod", ) msg = ChannelMessage( identity=identity, content="hi", metadata={"chat_type": "direct"}, ) result = resolve_session_route(msg) assert result class TestUrbitClient: def test_init(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" assert client.ship_name == "zod" assert client.auth_cookie_name == "urbauth-~zod" def test_cookies_none_when_no_session(self): from yuxi.channels.adapters.urbit.client import UrbitClient client = UrbitClient(ship_url="http://localhost:8080", ship_name="~zod") assert client._cookies() is None def test_cookies_with_session(self): from yuxi.channels.adapters.urbit.client import UrbitClient client = UrbitClient(ship_url="http://localhost:8080", ship_name="~zod") client.set_session_cookie("test-cookie") assert client._cookies() == {"urbauth-~zod": "test-cookie"} def test_http_access_before_start(self): from yuxi.channels.adapters.urbit.client import UrbitClient client = UrbitClient(ship_url="http://localhost:8080", ship_name="~zod") with pytest.raises(RuntimeError, match="not started"): _ = client.http class TestRateLimiter: @pytest.mark.asyncio async def test_acquire_returns_true_when_tokens_available(self): limiter = RateLimiter(limit=10, window=60.0) assert await limiter.acquire() @pytest.mark.asyncio async def test_acquire_exhausts_tokens(self): limiter = RateLimiter(limit=3, window=60.0) for _ in range(3): assert await limiter.acquire() assert not await limiter.acquire() @pytest.mark.asyncio async def test_acquire_refills_over_time(self): limiter = RateLimiter(limit=100, window=0.01) await limiter.acquire() assert await limiter.acquire() @pytest.mark.asyncio async def test_available_tokens(self): limiter = RateLimiter(limit=5, window=60.0) await limiter.acquire() assert limiter.available_tokens == 4.0 class TestExtractAttachments: def test_extract_image(self): additions = { "1": {"post": {"contents": [{"image": {"src": "https://img.com/a.png"}}]}} } result = extract_graph_attachments(additions) assert len(result) == 1 assert result[0]["type"] == "image" assert result[0]["url"] == "https://img.com/a.png" def test_extract_multiple(self): additions = { "1": {"post": {"contents": [ {"image": {"src": "https://img.com/a.png"}}, {"image": {"src": "https://img.com/b.png"}}, ]}} } result = extract_graph_attachments(additions) assert len(result) == 2 def test_extract_empty(self): assert extract_graph_attachments({}) == [] def test_extract_text_only(self): additions = {"1": {"post": {"contents": [{"text": "hello"}]}}} assert extract_graph_attachments(additions) == [] class TestPreConnect: def test_pre_connect_valid_config(self): import asyncio from yuxi.channels.adapters.urbit.adapter import UrbitAdapter from yuxi.channels.models import ChannelStatus config = { "ship_url": "http://localhost:8080", "ship_name": "~zod", "ship_code": "test-code", } adapter = UrbitAdapter(config=config) adapter._status = ChannelStatus.DISCONNECTED result = asyncio.run(adapter.pre_connect()) assert result["ship_url"] == "http://localhost:8080" assert result["ship_name"] == "zod" def test_pre_connect_missing_ship_url(self): import asyncio from yuxi.channels.adapters.urbit.adapter import UrbitAdapter from yuxi.channels.exceptions import ChannelAuthenticationError from yuxi.channels.models import ChannelStatus config = {"ship_name": "~zod", "ship_code": "test"} adapter = UrbitAdapter(config=config) adapter._status = ChannelStatus.DISCONNECTED with pytest.raises(ChannelAuthenticationError, match="ship_url"): asyncio.run(adapter.pre_connect()) def test_pre_connect_missing_ship_name(self): import asyncio from yuxi.channels.adapters.urbit.adapter import UrbitAdapter from yuxi.channels.exceptions import ChannelAuthenticationError from yuxi.channels.models import ChannelStatus config = {"ship_url": "http://localhost:8080", "ship_code": "test"} adapter = UrbitAdapter(config=config) adapter._status = ChannelStatus.DISCONNECTED with pytest.raises(ChannelAuthenticationError, match="ship_name"): asyncio.run(adapter.pre_connect()) def test_pre_connect_missing_ship_code(self): import asyncio from yuxi.channels.adapters.urbit.adapter import UrbitAdapter from yuxi.channels.exceptions import ChannelAuthenticationError from yuxi.channels.models import ChannelStatus config = {"ship_url": "http://localhost:8080", "ship_name": "~zod"} adapter = UrbitAdapter(config=config) adapter._status = ChannelStatus.DISCONNECTED with pytest.raises(ChannelAuthenticationError, match="ship_code"): asyncio.run(adapter.pre_connect()) class TestApprovalBlockCommand: def test_block_ship(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") assert approval.block_ship("~dev") is True assert approval.is_ship_blocked("~dev") is True assert approval.is_ship_blocked("dev") is True def test_unblock_ship(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") approval.block_ship("~dev") assert approval.unblock_ship("~dev") is True assert approval.is_ship_blocked("~dev") is False def test_unblock_nonexistent(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") assert approval.unblock_ship("~nonexistent") is False def test_get_blocked_ships(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") approval.block_ship("~dev") approval.block_ship("~marzod") blocked = approval.get_blocked_ships() assert "dev" in blocked assert "marzod" in blocked def test_handle_block_command(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") approval._pending["req-1"] = _make_pending_approval("req-1", "dm", "~dev") result = approval.handle_admin_command("block req-1", "zod") assert result is not None assert "Blocked" in result assert approval.is_ship_blocked("~dev") is True def test_handle_unblock_command(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") approval.block_ship("~dev") result = approval.handle_admin_command("unblock ~dev", "zod") assert result is not None assert "Unblocked" in result def test_handle_blocked_command(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") approval.block_ship("~dev") result = approval.handle_admin_command("blocked", "zod") assert result is not None assert "dev" in result def test_handle_pending_command(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") approval._pending["req-1"] = _make_pending_approval("req-1", "dm", "~dev") result = approval.handle_admin_command("pending", "zod") assert result is not None assert "req-1" in result def test_non_owner_cannot_block(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") approval._pending["req-1"] = _make_pending_approval("req-1", "dm", "~dev") result = approval.handle_admin_command("block req-1", "marzod") assert result is None def test_has_duplicate_pending(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") approval._pending["req-1"] = _make_pending_approval("req-1", "dm", "~dev", "chat-1") assert approval.has_duplicate_pending("dm", "~dev", "chat-1") is True assert approval.has_duplicate_pending("group", "~dev", "chat-1") is False assert approval.has_duplicate_pending("dm", "~other", "chat-1") is False class TestStoryRuleFormat: def test_rule_output_is_block_rule_format(self): from yuxi.channels.adapters.urbit.story import markdown_to_story result = markdown_to_story("text\n---\nmore text") found = False for item in result: if isinstance(item, dict) and "block" in item: if item["block"].get("rule") is None: found = True assert found, "Rule separator should produce {block: {rule: null}} format" class TestTargetFormat: def test_parse_tlon_dm_target(self): from yuxi.channels.adapters.urbit.targets import parse_tlon_target result = parse_tlon_target("tlon:dm/~zod") assert result["chat_type"] == "direct" assert result["peer_id"] == "~zod" def test_parse_group_chat_target(self): from yuxi.channels.adapters.urbit.targets import parse_tlon_target result = parse_tlon_target("group:chat/~zod/test-group") assert result["chat_type"] == "group" assert result["channel_type"] == "chat" def test_parse_group_target(self): from yuxi.channels.adapters.urbit.targets import parse_tlon_target result = parse_tlon_target("group:~zod/test-group") assert result["chat_type"] == "group" def test_format_target_hint_direct(self): from yuxi.channels.adapters.urbit.targets import format_target_hint hint = format_target_hint("direct", "~zod", "~host") assert "dm/~zod" in hint def test_format_target_hint_group(self): from yuxi.channels.adapters.urbit.targets import format_target_hint hint = format_target_hint("group", "test-group", "~host") assert "~host/test-group" in hint class TestSSRFValidation: def test_validate_local_url_blocked(self): from yuxi.channels.adapters.urbit.probe import validate_urbit_url is_valid, warnings = validate_urbit_url("http://127.0.0.1/test") assert is_valid is False assert len(warnings) > 0 def test_validate_local_url_allowed_when_allow_private(self): from yuxi.channels.adapters.urbit.probe import validate_urbit_url is_valid, _ = validate_urbit_url("http://127.0.0.1/test", allow_private=True) assert is_valid is True def test_ssrf_policy_default(self): from yuxi.channels.adapters.urbit.probe import ssrf_policy_from_dangerously_allow_private_network policy = ssrf_policy_from_dangerously_allow_private_network() assert policy["allowPrivate"] is False assert policy["dangerouslyAllowPrivateNetwork"] is False def test_ssrf_policy_dangerous(self): from yuxi.channels.adapters.urbit.probe import ssrf_policy_from_dangerously_allow_private_network policy = ssrf_policy_from_dangerously_allow_private_network(dangerously_allow_private_network=True) assert policy["allowPrivate"] is True class TestDoctorMigrations: def test_detect_legacy_allow_private_network(self): from yuxi.channels.adapters.urbit.doctor import is_legacy_private_network_config assert is_legacy_private_network_config({"allowPrivateNetwork": True}) is True assert is_legacy_private_network_config({"other": True}) is False def test_create_migrations(self): from yuxi.channels.adapters.urbit.doctor import create_legacy_private_network_doctor_contract migrations = create_legacy_private_network_doctor_contract({"allowPrivateNetwork": True}) assert len(migrations) == 1 assert migrations[0]["from"] == "allowPrivateNetwork" assert migrations[0]["to"] == "network.dangerouslyAllowPrivateNetwork" def test_apply_migrations(self): from yuxi.channels.adapters.urbit.doctor import create_legacy_private_network_doctor_contract, apply_doctor_migrations config = {"allowPrivateNetwork": True} migrations = create_legacy_private_network_doctor_contract(config) result = apply_doctor_migrations(config, migrations) assert "allowPrivateNetwork" not in result assert result["network"]["dangerously_allow_private_network"] is True def _make_pending_approval( aid: str, atype: str, ship: str, channel: str = "chat-1" ): from yuxi.channels.adapters.urbit.approval import PendingApproval return PendingApproval( id=aid, approval_type=atype, requester_ship=ship, channel_id=channel, message_content="test message", created_at=1700000000.0, status="pending", ) class TestSanitizeFilename: def test_sanitize_normal(self): from yuxi.channels.adapters.urbit.upload import sanitize_filename assert sanitize_filename("image.png") == "image.png" def test_sanitize_path_traversal(self): from yuxi.channels.adapters.urbit.upload import sanitize_filename result = sanitize_filename("../../../etc/passwd") assert ".." not in result assert result == "passwd" def test_sanitize_windows_path(self): from yuxi.channels.adapters.urbit.upload import sanitize_filename result = sanitize_filename("C:\\Windows\\System32\\cmd.exe") assert "\\" not in result assert ":" not in result def test_sanitize_special_chars(self): from yuxi.channels.adapters.urbit.upload import sanitize_filename result = sanitize_filename("file:name*.txt") assert ":" not in result assert "*" not in result assert result == "file_name_.txt" def test_sanitize_all_special_returns_hash(self): from yuxi.channels.adapters.urbit.upload import sanitize_filename result = sanitize_filename("...") assert result.startswith("file_") assert len(result) > len("file_") def test_sanitize_long_name(self): from yuxi.channels.adapters.urbit.upload import sanitize_filename long_name = "a" * 300 + ".txt" result = sanitize_filename(long_name) assert len(result) <= 255 assert result.endswith(".txt") class TestMediaExtensions: def test_heic_extension(self): from yuxi.channels.adapters.urbit.media import get_media_extension assert get_media_extension("image/heic") == ".heic" def test_heif_extension(self): from yuxi.channels.adapters.urbit.media import get_media_extension assert get_media_extension("image/heif") == ".heif" def test_unknown_extension(self): from yuxi.channels.adapters.urbit.media import get_media_extension assert get_media_extension("application/octet-stream") == ".bin" class TestConfigUIHints: def test_get_hints_returns_list(self): from yuxi.channels.adapters.urbit.config_ui_hints import get_config_ui_hints hints = get_config_ui_hints() assert isinstance(hints, list) assert len(hints) >= 30 def test_every_hint_has_key_and_label(self): from yuxi.channels.adapters.urbit.config_ui_hints import get_config_ui_hints for hint in get_config_ui_hints(): assert "key" in hint assert "label" in hint assert "type" in hint def test_required_fields_exist(self): from yuxi.channels.adapters.urbit.config_ui_hints import get_config_ui_hints hints = get_config_ui_hints() required_keys = {h["key"] for h in hints if h.get("required")} assert "ship_url" in required_keys assert "ship_name" in required_keys assert "ship_code" in required_keys def test_get_hints_by_category(self): from yuxi.channels.adapters.urbit.config_ui_hints import get_hints_by_category grouped = get_hints_by_category() assert isinstance(grouped, dict) assert "connection" in grouped assert len(grouped["connection"]) >= 3 def test_get_categories(self): from yuxi.channels.adapters.urbit.config_ui_hints import get_categories categories = get_categories() assert "connection" in categories assert "authorization" in categories assert "performance" in categories def test_count_hints(self): from yuxi.channels.adapters.urbit.config_ui_hints import count_hints assert count_hints() >= 30 class TestDualPlugin: def test_setup_plugin_validate(self): from yuxi.channels.adapters.urbit.dual_plugin import UrbitSetupPlugin plugin = UrbitSetupPlugin({}) errors = plugin.validate_setup() assert any("ship_url" in e for e in errors) assert any("ship_name" in e for e in errors) assert any("ship_code" in e for e in errors) def test_setup_plugin_valid(self): from yuxi.channels.adapters.urbit.dual_plugin import UrbitSetupPlugin plugin = UrbitSetupPlugin({ "ship_url": "http://localhost:8080", "ship_name": "zod", "ship_code": "XXXX-XXXX-XXXX-XXXX", }) errors = plugin.validate_setup() assert len(errors) == 0 def test_setup_plugin_invalid_policy(self): from yuxi.channels.adapters.urbit.dual_plugin import UrbitSetupPlugin plugin = UrbitSetupPlugin({ "ship_url": "http://localhost:8080", "ship_name": "zod", "ship_code": "XXXX", "dm_policy": "invalid", }) errors = plugin.validate_setup() assert any("dm_policy" in e for e in errors) def test_detect_legacy_migrations(self): from yuxi.channels.adapters.urbit.dual_plugin import UrbitSetupPlugin plugin = UrbitSetupPlugin({"allowPrivateNetwork": True}) migrations = plugin.detect_legacy_state_migrations() assert len(migrations) == 1 assert plugin.has_legacy_migrations is True def test_get_setup_schema(self): from yuxi.channels.adapters.urbit.dual_plugin import UrbitSetupPlugin plugin = UrbitSetupPlugin({}) schema = plugin.get_setup_schema() assert schema["name"] == "urbit" assert len(schema["required_fields"]) == 3 def test_get_config_ui_hints(self): from yuxi.channels.adapters.urbit.dual_plugin import UrbitSetupPlugin plugin = UrbitSetupPlugin({}) hints = plugin.get_config_ui_hints() assert len(hints) >= 30 def test_registry_register_and_get(self): from yuxi.channels.adapters.urbit.dual_plugin import DualPluginRegistry, UrbitSetupPlugin registry = DualPluginRegistry() setup = UrbitSetupPlugin({"ship_url": "http://localhost:8080"}) registry.register_setup_plugin(setup) assert registry.get_setup_plugin() is setup def test_registry_clear(self): from yuxi.channels.adapters.urbit.dual_plugin import DualPluginRegistry, UrbitSetupPlugin registry = DualPluginRegistry() registry.register_setup_plugin(UrbitSetupPlugin({})) registry.clear() assert registry.get_setup_plugin() is None def test_global_registry(self): from yuxi.channels.adapters.urbit.dual_plugin import get_global_registry, DualPluginRegistry registry = get_global_registry() assert isinstance(registry, DualPluginRegistry) class TestMultiAccount: def test_account_manager_has_multi(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.has_multi_accounts() is True assert mgr.count == 2 def test_account_manager_single(self): from yuxi.channels.adapters.urbit.accounts import AccountManager mgr = AccountManager([ {"ship": "zod", "url": "http://localhost", "code": "code"}, ]) assert mgr.has_multi_accounts() is False def test_multi_account_connection_manager_init(self): from yuxi.channels.adapters.urbit.accounts import AccountManager, MultiAccountConnectionManager mgr = AccountManager() conn_mgr = MultiAccountConnectionManager(mgr, lambda a: a) assert not conn_mgr.is_running assert len(conn_mgr.connections) == 0 def test_multi_account_get_nonexistent(self): from yuxi.channels.adapters.urbit.accounts import AccountManager, MultiAccountConnectionManager mgr = AccountManager() conn_mgr = MultiAccountConnectionManager(mgr, lambda a: a) assert conn_mgr.get_connection("nonexistent") is None class TestSettingsStoreJSONBoundary: 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 {"moltbot": {}} 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_parse_channel_rules_json_string(self): from yuxi.channels.adapters.urbit.settings import parse_channel_rules result = parse_channel_rules('{"chat/my-group": {"allow": ["zod"]}}') assert isinstance(result, dict) assert "chat/my-group" in result def test_parse_channel_rules_dict(self): from yuxi.channels.adapters.urbit.settings import parse_channel_rules data = {"chat/my-group": {"allow": ["zod"]}} result = parse_channel_rules(data) assert result == data def test_parse_channel_rules_none(self): from yuxi.channels.adapters.urbit.settings import parse_channel_rules assert parse_channel_rules(None) is None def test_parse_channel_rules_invalid_json(self): from yuxi.channels.adapters.urbit.settings import parse_channel_rules result = parse_channel_rules("{invalid") assert result is None def test_parse_pending_approvals(self): from yuxi.channels.adapters.urbit.settings import parse_pending_approvals result = parse_pending_approvals('[{"id": "a1", "status": "pending"}]') assert isinstance(result, list) assert len(result) == 1 def test_parse_pending_approvals_none(self): from yuxi.channels.adapters.urbit.settings import parse_pending_approvals assert parse_pending_approvals(None) is None def test_serialize_to_json(self): from yuxi.channels.adapters.urbit.settings import serialize_to_json_value result = serialize_to_json_value({"key": "val"}) assert isinstance(result, str) assert '"key"' in result class TestSummarizer: def test_trigger_detection_summarize(self): from yuxi.channels.adapters.urbit.summarizer import Summarizer from yuxi.channels.adapters.urbit.history import MessageCache cache = MessageCache() summ = Summarizer(cache) result = summ.is_summarization_request("summarize this chat") assert result is True def test_trigger_detection_tldr(self): from yuxi.channels.adapters.urbit.summarizer import Summarizer from yuxi.channels.adapters.urbit.history import MessageCache cache = MessageCache() summ = Summarizer(cache) result = summ.is_summarization_request("tldr") assert result is True def test_trigger_detection_recap(self): from yuxi.channels.adapters.urbit.summarizer import Summarizer from yuxi.channels.adapters.urbit.history import MessageCache cache = MessageCache() summ = Summarizer(cache) result = summ.is_summarization_request("what did i miss") assert result is True def test_trigger_no_summary(self): from yuxi.channels.adapters.urbit.summarizer import Summarizer from yuxi.channels.adapters.urbit.history import MessageCache cache = MessageCache() summ = Summarizer(cache) result = summ.is_summarization_request("hello world") assert result is False def test_auto_detect(self): import asyncio from yuxi.channels.adapters.urbit.summarizer import Summarizer from yuxi.channels.adapters.urbit.history import MessageCache cache = MessageCache() asyncio.run(cache.cache_message("m1", "chat-1", "hello", "zod")) summ = Summarizer(cache) info = summ.auto_detect_and_respond("chat-1", "catch up") assert info is not None assert info.get("channel_id") == "chat-1" def test_auto_detect_no_match(self): from yuxi.channels.adapters.urbit.summarizer import Summarizer from yuxi.channels.adapters.urbit.history import MessageCache cache = MessageCache() summ = Summarizer(cache) info = summ.auto_detect_and_respond("chat-1", "hello world") assert info is None class TestCitesParsing: def test_parse_chan_cite(self): from yuxi.channels.adapters.urbit.cites import parse_cite result = parse_cite({"cite": "chan", "group": "test", "chan": "mychan"}) assert result is not None assert result.get("type") == "chan" assert result.get("group") == "test" def test_parse_empty(self): from yuxi.channels.adapters.urbit.cites import parse_cite assert parse_cite({}) is None assert parse_cite({"text": "hello"}) is None class TestApprovalPersistence: def _make_mock_client(self): class MockResp: status_code = 200 def json(self): return {"pending_approvals": [], "blocked_ships": []} def raise_for_status(self): pass class MockClient: ship_name = "zod" ship_url = "http://localhost" async def get(self, path, **kwargs): return MockResp() async def put(self, path, **kwargs): return MockResp() return MockClient() @pytest.mark.asyncio async def test_load_from_empty_store(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") loaded = await approval.load_from_settings_store(self._make_mock_client()) assert isinstance(loaded, list) @pytest.mark.asyncio async def test_persist_to_store(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") approval.create_pending("dm", "~dev", "chat-1", "hello") await approval.persist_to_settings_store(self._make_mock_client()) class TestApprovalDedupIntegration: def test_create_pending_then_check_duplicate(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") approval.create_pending("dm", "~dev", "chat-1", "hello") assert approval.has_duplicate_pending("dm", "~dev", "chat-1") is True assert approval.has_duplicate_pending("dm", "~dev", "chat-2") is False assert approval.has_duplicate_pending("channel", "~dev", "chat-1") is False def test_approve_removes_from_pending(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") p = approval.create_pending("dm", "~dev", "chat-1", "hello") result = approval.approve(p.id) assert result is not None assert result.status == "approved" assert approval.has_duplicate_pending("dm", "~dev", "chat-1") is False def test_deny_removes_from_pending(self): from yuxi.channels.adapters.urbit.approval import ApprovalSystem approval = ApprovalSystem(owner_ship="~zod") p = approval.create_pending("dm", "~dev", "chat-1", "hello") result = approval.deny(p.id) assert result is not None assert result.status == "denied" assert approval.has_duplicate_pending("dm", "~dev", "chat-1") is False