ForcePilot/backend/test/unit/channels/test_urbit_adapter.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

1176 lines
42 KiB
Python

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