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

1787 lines
59 KiB
Python
Raw Normal View History

from __future__ import annotations
import os
import time
from unittest.mock import MagicMock, patch
import pytest
from yuxi.channels.adapters.irc.normalize import (
build_allowlist_candidates,
normalize_allow_entry,
looks_like_irc_target_id,
normalize_irc_messaging_target,
resolve_targets,
format_irc_sender_id,
resolve_allowlist_match,
)
from yuxi.channels.adapters.irc.accounts import (
resolve_merged_account_config,
list_irc_account_ids,
list_enabled_irc_accounts,
get_account_config,
is_configured,
resolve_default_irc_account_id,
)
from yuxi.channels.adapters.irc.known_servers import (
KNOWN_IRC_SERVERS,
list_known_servers,
lookup_server,
get_default_server,
)
from yuxi.channels.adapters.irc.secret_contract import (
IRC_SECRET_TARGETS,
SecretTarget,
list_secret_targets,
get_secret_target,
audit_secrets,
)
from yuxi.channels.adapters.irc.directory import (
DirectoryUser,
DirectoryGroup,
list_users,
list_groups,
)
from yuxi.channels.adapters.irc.doctor import (
DoctorDiagnosis,
diagnose,
)
from yuxi.channels.adapters.irc.config_schema import (
get_config_schema,
get_schema_keys,
validate_config,
)
from yuxi.channels.adapters.irc.setup_core import (
update_irc_account_config,
remove_irc_account_config,
set_irc_server_config,
set_irc_nick_config,
set_irc_auth_config,
)
from yuxi.channels.adapters.irc.setup_entry import (
get_setup_entry,
register_bundled_channel_setup,
)
from yuxi.channels.adapters.irc.commands import (
should_handle_text_commands,
has_control_command,
parse_command,
check_command_access,
handle_command,
)
from yuxi.channels.adapters.irc.security import (
IRCSecurityConfig,
SecurityCheckResult,
collect_security_warnings,
_find_mutable_allowlist_entries,
)
from yuxi.channels.adapters.irc.send_cache import (
SentMessageCache,
SentMessageEntry,
)
from yuxi.channels.adapters.irc.formatter import (
format_outbound,
convert_markdown_tables,
)
from yuxi.channels.adapters.irc.nickserv import (
nickserv_identify,
nickserv_ghost,
nickserv_recover,
nickserv_release,
nickserv_info,
nickserv_status,
)
from yuxi.channels.adapters.irc.media_vision import (
extract_media_urls,
has_media_url,
analyze_urls,
)
from yuxi.channels.adapters.irc.protocol import (
parse_irc_tags_dict,
extract_account_from_tags,
is_identified,
parse_irc_tags,
)
from yuxi.channels.adapters.irc.channel_runtime import ChannelRuntime, _merge_modes
from yuxi.channels.adapters.irc.splits import (
split_markdown,
split_irc_text,
)
from yuxi.channels.adapters.irc.auth import (
resolve_password,
get_server_password,
nick_recover,
)
from yuxi.channels.adapters.irc.ctcp import (
handle_ctcp,
extract_ctcp_action,
supports_ctcp,
)
from yuxi.channels.adapters.irc.config_ui_hints import get_config_ui_hints
from yuxi.channels.models import (
ChannelIdentity,
ChannelResponse,
ChannelType,
)
# ========================================================================
# normalize.py - Allowlist 候选/匹配/规范化
# ========================================================================
class TestBuildAllowlistCandidates:
def test_basic_nick(self):
candidates = build_allowlist_candidates("alice")
assert candidates == ["alice"]
def test_with_user(self):
candidates = build_allowlist_candidates("alice", sender_user="user1")
assert "alice!user1" in candidates
def test_with_host(self):
candidates = build_allowlist_candidates("alice", sender_host="example.com")
assert "alice@example.com" in candidates
def test_with_user_and_host(self):
candidates = build_allowlist_candidates("alice", "user1", "example.com")
assert "alice!user1@example.com" in candidates
assert len(candidates) == 4
def test_special_chars_in_nick(self):
candidates = build_allowlist_candidates("nick[name]")
assert "nick[name]" in candidates
def test_empty_user_and_host(self):
candidates = build_allowlist_candidates("alice", None, None)
assert candidates == ["alice"]
class TestResolveAllowlistMatch:
def test_exact_match(self):
result = resolve_allowlist_match(["alice"], ["alice", "bob"])
assert result == "alice"
def test_no_match(self):
result = resolve_allowlist_match(["charlie"], ["alice", "bob"])
assert result is None
def test_wildcard_star(self):
result = resolve_allowlist_match(["anyone"], ["*"])
assert result == "*"
def test_glob_match(self):
result = resolve_allowlist_match(["alice!user@host"], ["alice!*@*"])
assert result == "alice!*@*"
def test_multiple_candidates_first_wins(self):
candidates = ["alice!user@host", "alice@host", "alice"]
result = resolve_allowlist_match(candidates, ["alice"])
assert result == "alice"
def test_full_mask_before_wildcard(self):
candidates = ["bob!user@host"]
result = resolve_allowlist_match(candidates, ["bob!user@host", "*"])
assert result == "bob!user@host"
def test_empty_allowlist(self):
result = resolve_allowlist_match(["alice"], [])
assert result is None
def test_empty_candidates(self):
result = resolve_allowlist_match([], ["alice"])
assert result is None
def test_glob_match_host(self):
result = resolve_allowlist_match(["alice@trusted.com"], ["*@trusted.com"])
assert result == "*@trusted.com"
class TestNormalizeAllowEntry:
def test_strips_irc_prefix(self):
assert normalize_allow_entry("irc:alice") == "alice"
assert normalize_allow_entry("irc:user:alice") == "alice"
def test_strips_user_prefix(self):
assert normalize_allow_entry("user:alice") == "alice"
def test_no_prefix_unchanged(self):
assert normalize_allow_entry("alice") == "alice"
def test_mask_with_at(self):
assert normalize_allow_entry("alice@host") == "alice@host"
class TestLooksLikeIrcTargetId:
def test_channel_prefix(self):
assert looks_like_irc_target_id("irc:channel:#test") is True
def test_user_prefix(self):
assert looks_like_irc_target_id("irc:user:alice") is True
def test_full_mask(self):
assert looks_like_irc_target_id("nick!user@host") is True
def test_plain_nick(self):
assert looks_like_irc_target_id("alice") is False
def test_channel_hash(self):
assert looks_like_irc_target_id("#channel") is False
class TestNormalizeIrcMessagingTarget:
def test_channel_prefix(self):
assert normalize_irc_messaging_target("irc:channel:#test") == ("group", "#test")
def test_user_prefix(self):
assert normalize_irc_messaging_target("irc:user:alice") == ("direct", "alice")
def test_hash_channel(self):
assert normalize_irc_messaging_target("#channel") == ("group", "#channel")
def test_ampersand_channel(self):
assert normalize_irc_messaging_target("&channel") == ("group", "&channel")
def test_plain_nick(self):
assert normalize_irc_messaging_target("alice") == ("direct", "alice")
def test_nick_with_irc_prefix(self):
assert normalize_irc_messaging_target("irc:alice") == ("direct", "alice")
class TestResolveTargets:
def test_channel_target(self):
result = resolve_targets("#test")
assert result["kind"] == "group"
assert result["id"] == "#test"
def test_user_target(self):
result = resolve_targets("alice")
assert result["kind"] == "direct"
class TestFormatIrcSenderId:
def test_full(self):
assert format_irc_sender_id("nick", "user", "host") == "nick!user@host"
def test_user_only(self):
assert format_irc_sender_id("nick", "user") == "nick!user"
def test_host_only(self):
assert format_irc_sender_id("nick", host="host") == "nick@host"
def test_nick_only(self):
assert format_irc_sender_id("nick") == "nick"
def test_none_user_and_host(self):
assert format_irc_sender_id("nick", None, None) == "nick"
# ========================================================================
# accounts.py - 多账户/合并/解析
# ========================================================================
class TestAccountsResolveMergedConfig:
def test_basic_merge(self):
base = {"server": "irc.test.com", "nick": "basebot"}
account = {"nick": "accountbot", "port": 7000}
merged = resolve_merged_account_config(base, account)
assert merged["server"] == "irc.test.com"
assert merged["nick"] == "accountbot"
assert merged["port"] == 7000
def test_deep_merge(self):
base = {"commands": {"enabled": False, "custom": {}}}
account = {"commands": {"enabled": True}}
merged = resolve_merged_account_config(base, account)
assert merged["commands"]["enabled"] is True
assert "custom" in merged["commands"]
def test_original_unchanged(self):
base = {"server": "irc.test.com"}
account = {"port": 7000}
resolve_merged_account_config(base, account)
assert "port" not in base
def test_empty_override(self):
base = {"server": "irc.test.com", "nick": "bot"}
merged = resolve_merged_account_config(base, {})
assert merged == base
assert merged is not base
class TestAccountsListIds:
def test_normal_accounts(self):
config = {"accounts": {"default": {}, "alt": {}}}
assert list_irc_account_ids(config) == ["default", "alt"]
def test_no_accounts(self):
assert list_irc_account_ids({}) == []
def test_accounts_not_dict(self):
assert list_irc_account_ids({"accounts": "string"}) == []
class TestAccountsListEnabled:
def test_enabled_accounts(self):
config = {
"server": "irc.test.com",
"nick": "topbot",
"accounts": {
"default": {"enabled": True},
"disabled_one": {"enabled": False},
"implicit": {},
},
}
enabled = list_enabled_irc_accounts(config)
assert len(enabled) == 2
ids = {a["_account_id"] for a in enabled}
assert ids == {"default", "implicit"}
def test_merged_inherits_base(self):
config = {
"server": "irc.test.com",
"nick": "topbot",
"accounts": {"default": {}},
}
enabled = list_enabled_irc_accounts(config)
assert enabled[0]["server"] == "irc.test.com"
assert enabled[0]["nick"] == "topbot"
def test_no_accounts(self):
assert list_enabled_irc_accounts({}) == []
class TestAccountsGetConfig:
def test_existing_account(self):
config = {
"server": "irc.test.com",
"nick": "topbot",
"accounts": {"default": {"nick": "override"}},
}
account = get_account_config(config, "default")
assert account["nick"] == "override"
assert account["server"] == "irc.test.com"
def test_disabled_account(self):
config = {
"accounts": {"default": {"enabled": False}},
}
assert get_account_config(config, "default") is None
def test_nonexistent_account(self):
assert get_account_config({}, "nonexistent") is None
class TestAccountsIsConfigured:
def test_config_only(self):
assert is_configured({"server": "irc.test.com", "nick": "bot"}) is True
def test_missing_nick(self):
assert is_configured({"server": "irc.test.com"}) is False
def test_missing_server(self):
assert is_configured({"nick": "bot"}) is False
def test_env_fallback(self, monkeypatch):
monkeypatch.setenv("IRC_HOST", "irc.test.com")
monkeypatch.setenv("IRC_NICK", "envbot")
assert is_configured({}) is True
def test_config_overrides_env(self, monkeypatch):
monkeypatch.setenv("IRC_HOST", "wrong.example.com")
assert is_configured({"server": "correct.com", "nick": "bot"}) is True
class TestAccountsResolveDefaultId:
def test_default_exists(self):
config = {"accounts": {"default": {}, "alt": {}}}
assert resolve_default_irc_account_id(config) == "default"
def test_no_default_uses_first(self):
config = {"accounts": {"alt1": {}, "alt2": {}}}
assert resolve_default_irc_account_id(config) == "alt1"
def test_no_accounts(self):
assert resolve_default_irc_account_id({}) == "default"
def test_custom_default(self):
config = {"accounts": {"main": {}}}
assert resolve_default_irc_account_id(config, default="main") == "main"
# ========================================================================
# known_servers.py
# ========================================================================
class TestKnownServers:
def test_known_servers_size(self):
assert len(KNOWN_IRC_SERVERS) >= 8
def test_list_known_servers(self):
servers = list_known_servers()
assert len(servers) >= 8
for s in servers:
assert "name" in s
assert "host" in s
assert "port" in s
assert "tls" in s
def test_lookup_exact(self):
server = lookup_server("Libera.Chat")
assert server is not None
assert server["host"] == "irc.libera.chat"
assert server["port"] == 6697
def test_lookup_case_insensitive(self):
server = lookup_server("libera.chat")
assert server is not None
assert server["host"] == "irc.libera.chat"
def test_lookup_nonexistent(self):
assert lookup_server("nonexistent") is None
def test_get_default_server(self):
default = get_default_server()
assert default["name"] == "Libera.Chat"
assert default["host"] == "irc.libera.chat"
def test_twitch_server(self):
server = lookup_server("Twitch")
assert server is not None
assert server["host"] == "irc.chat.twitch.tv"
# ========================================================================
# secret_contract.py
# ========================================================================
class TestSecretTarget:
def test_dataclass_creation(self):
st = SecretTarget(
key="test_key",
label="Test Label",
description="A test secret",
env_var="TEST_ENV",
)
assert st.key == "test_key"
assert st.label == "Test Label"
assert st.env_var == "TEST_ENV"
class TestListSecretTargets:
def test_returns_all_targets(self):
targets = list_secret_targets()
assert len(targets) == len(IRC_SECRET_TARGETS)
keys = {t["key"] for t in targets}
assert "nickserv_password" in keys
assert "sasl_password" in keys
assert "server_password" in keys
def test_target_structure(self):
targets = list_secret_targets()
for t in targets:
assert "key" in t
assert "label" in t
assert "description" in t
assert "env_var" in t
assert "config_path" in t
class TestGetSecretTarget:
def test_existing_key(self):
target = get_secret_target("sasl_password")
assert target is not None
assert target["key"] == "sasl_password"
assert target["env_var"] == "IRC_SASL_PASSWORD"
def test_nonexistent_key(self):
assert get_secret_target("nonexistent") is None
class TestAuditSecrets:
def test_all_configured(self):
config = {
"sasl_password": "secret1",
"nickserv_password": "secret2",
"password": "serverpass",
"nickserv_password_file": "/path/to/file",
}
findings = audit_secrets(config)
for f in findings:
assert f["key"] in {t.key for t in IRC_SECRET_TARGETS}
assert "configured" in f
assert "sources" in f
def test_nothing_configured(self):
findings = audit_secrets({})
assert all(not f["configured"] for f in findings)
def test_env_var_detection(self, monkeypatch):
monkeypatch.setenv("IRC_SASL_PASSWORD", "env_secret")
findings = audit_secrets({})
sasl = next(f for f in findings if f["key"] == "sasl_password")
assert sasl["configured"] is True
assert any("env.IRC_SASL_PASSWORD" in s for s in sasl["sources"])
# ========================================================================
# directory.py
# ========================================================================
class TestDirectoryDataclasses:
def test_directory_user(self):
user = DirectoryUser(id="alice", display_name="Alice")
assert user.id == "alice"
assert user.display_name == "Alice"
def test_directory_group(self):
group = DirectoryGroup(id="#test", display_name="#test", member_count=42)
assert group.id == "#test"
assert group.member_count == 42
class TestListUsers:
def test_from_allow_from(self):
config = {"allowFrom": ["alice!user@host", "bob@example.com"]}
users = list_users(config)
assert len(users) == 2
names = {u.id for u in users}
assert names == {"alice", "bob"}
def test_from_group_allow_from(self):
config = {"groupAllowFrom": ["charlie!user@host"]}
users = list_users(config)
assert len(users) == 1
assert users[0].id == "charlie"
def test_from_groups_allow_from(self):
config = {
"groups": {
"#test": {"allowFrom": ["dave!user@host"]},
}
}
users = list_users(config)
assert len(users) == 1
assert users[0].id == "dave"
def test_dedup_across_sources(self):
config = {
"allowFrom": ["alice!user@host"],
"groupAllowFrom": ["alice@example.com"],
}
users = list_users(config)
assert len(users) == 1
def test_ignore_star(self):
config = {"allowFrom": ["*"]}
users = list_users(config)
assert len(users) == 0
def test_empty_entry(self):
config = {"allowFrom": [""]}
users = list_users(config)
assert len(users) == 0
def test_sorted_alphabetically(self):
config = {"allowFrom": ["zoe!x@y", "alice!x@y"]}
users = list_users(config)
assert users[0].id == "alice"
assert users[1].id == "zoe"
class TestListGroups:
def test_from_auto_join_channels(self):
config = {"auto_join_channels": ["#general", "&support"]}
groups = list_groups(config)
assert len(groups) == 2
names = {g.id for g in groups}
assert names == {"#general", "&support"}
def test_with_group_config(self):
config = {
"auto_join_channels": ["#test"],
"groups": {"#test": {"skills": ["skill1"], "enabled": True}},
}
groups = list_groups(config)
assert len(groups) == 1
assert groups[0].metadata == {"skills": ["skill1"], "systemPrompt": None, "enabled": True}
def test_extra_groups(self):
config = {
"groups": {"extra_group": {"some": "config"}},
}
groups = list_groups(config)
ids = {g.id for g in groups}
assert "extra_group" in ids
def test_skip_channel_names_in_groups(self):
config = {
"groups": {"*": {}, "#already_in_channels": {}, "&also": {}},
}
groups = list_groups(config)
assert len(groups) == 0
def test_env_channels(self, monkeypatch):
monkeypatch.setenv("IRC_CHANNELS", "#env1, #env2")
config = {}
groups = list_groups(config)
assert len(groups) == 2
# ========================================================================
# doctor.py
# ========================================================================
class TestDoctorDiagnosis:
def test_default_ok(self):
d = DoctorDiagnosis()
assert d.status == "ok"
assert d.warnings == []
assert d.errors == []
assert d.checks == []
class TestDiagnose:
def test_tls_enabled(self):
result = diagnose({}, use_tls=True)
tls_check = next(c for c in result.checks if c["name"] == "tls")
assert tls_check["status"] == "ok"
def test_tls_disabled(self):
result = diagnose({}, use_tls=False)
tls_check = next(c for c in result.checks if c["name"] == "tls")
assert tls_check["status"] == "warning"
assert len(result.warnings) >= 1
def test_nickserv_enabled(self):
result = diagnose({}, nickserv_enabled=True)
ns_check = next(c for c in result.checks if c["name"] == "nickserv")
assert ns_check["status"] == "ok"
def test_nickserv_disabled(self):
result = diagnose({}, nickserv_enabled=False)
ns_check = next(c for c in result.checks if c["name"] == "nickserv")
assert ns_check["status"] == "warning"
def test_nickserv_register_warning(self):
result = diagnose({}, nickserv_enabled=True, nickserv_register=True)
ns_check = next(c for c in result.checks if c["name"] == "nickserv")
assert ns_check["status"] == "warning"
def test_open_group_policy_warning(self):
sec_config = IRCSecurityConfig(group_policy="open")
result = diagnose({}, security_config=sec_config)
gp_check = next(c for c in result.checks if c["name"] == "group_policy")
assert gp_check["status"] == "warning"
def test_mutable_allowlist_warning(self):
sec_config = IRCSecurityConfig(allow_from=["alice"])
result = diagnose({}, security_config=sec_config)
assert any("Mutable" in w for w in result.warnings)
def test_no_security_config(self):
result = diagnose({})
assert result.checks
# ========================================================================
# config_schema.py
# ========================================================================
class TestGetConfigSchema:
def test_returns_dict(self):
schema = get_config_schema()
assert isinstance(schema, dict)
assert "server" in schema
assert "nick" in schema
assert "port" in schema
def test_get_schema_keys(self):
keys = get_schema_keys()
assert "server" in keys
assert "nick" in keys
assert len(keys) > 20
class TestValidateConfig:
def test_valid_minimal_config(self):
errors = validate_config({"server": "irc.test.com", "nick": "bot"})
assert errors == []
def test_missing_server(self):
errors = validate_config({"nick": "bot"})
assert any("server" in e for e in errors)
def test_missing_nick(self):
errors = validate_config({"server": "irc.test.com"})
assert any("nick" in e for e in errors)
def test_invalid_dm_policy(self):
errors = validate_config({
"server": "irc.test.com",
"nick": "bot",
"dm_policy": "invalid",
})
assert any("dm_policy" in e for e in errors)
def test_invalid_group_policy(self):
errors = validate_config({
"server": "irc.test.com",
"nick": "bot",
"group_policy": "invalid",
})
assert any("group_policy" in e for e in errors)
def test_invalid_port_type(self):
errors = validate_config({
"server": "irc.test.com",
"nick": "bot",
"port": "not_a_number",
})
assert any("port" in e for e in errors)
def test_port_too_low(self):
errors = validate_config({
"server": "irc.test.com",
"nick": "bot",
"port": 0,
})
assert any("port" in e for e in errors)
def test_port_too_high(self):
errors = validate_config({
"server": "irc.test.com",
"nick": "bot",
"port": 99999,
})
assert any("port" in e for e in errors)
def test_invalid_chunker_mode(self):
errors = validate_config({
"server": "irc.test.com",
"nick": "bot",
"chunker_mode": "invalid",
})
assert any("chunker_mode" in e for e in errors)
def test_nicserv_register_missing_email(self):
errors = validate_config({
"server": "irc.test.com",
"nick": "bot",
"nickserv_register": True,
})
assert any("nickserv_register_email" in e for e in errors)
def test_nicserv_register_with_email(self):
errors = validate_config({
"server": "irc.test.com",
"nick": "bot",
"nickserv_register": True,
"nickserv_register_email": "bot@test.com",
})
assert not any("nickserv_register_email" in e for e in errors)
def test_dm_policy_open_without_star(self):
errors = validate_config({
"server": "irc.test.com",
"nick": "bot",
"dm_policy": "open",
"allowFrom": [],
})
assert any("allowFrom" in e for e in errors)
def test_dotted_keys_in_config(self):
errors = validate_config({
"server": "irc.test.com",
"nick": "bot",
"commands": {"enabled": True},
})
assert len(errors) == 0
# ========================================================================
# setup_core.py
# ========================================================================
class TestUpdateIrcAccountConfig:
def test_update_simple(self):
existing = {"server": "old.server.com", "nick": "oldbot"}
result = update_irc_account_config(existing, {"server": "new.server.com"})
assert result["server"] == "new.server.com"
assert result["nick"] == "oldbot"
def test_does_not_mutate_original(self):
existing = {"server": "old.server.com"}
update_irc_account_config(existing, {"server": "new.server.com"})
assert existing["server"] == "old.server.com"
def test_deep_merge(self):
existing = {"commands": {"enabled": False, "custom": {"a": "b"}}}
result = update_irc_account_config(existing, {"commands": {"enabled": True}})
assert result["commands"]["enabled"] is True
assert result["commands"]["custom"] == {"a": "b"}
class TestRemoveIrcAccountConfig:
def test_remove_existing_keys(self):
existing = {"server": "irc.test.com", "nick": "bot", "port": 6667}
result = remove_irc_account_config(existing, ["port"])
assert "port" not in result
assert "server" in result
def test_remove_nonexistent_key(self):
existing = {"server": "irc.test.com"}
result = remove_irc_account_config(existing, ["nonexistent"])
assert result == {"server": "irc.test.com"}
def test_remove_multiple_keys(self):
existing = {"a": 1, "b": 2, "c": 3}
result = remove_irc_account_config(existing, ["a", "b"])
assert result == {"c": 3}
def test_does_not_mutate_original(self):
existing = {"server": "irc.test.com", "port": 6667}
remove_irc_account_config(existing, ["port"])
assert "port" in existing
class TestSetIrcServerConfig:
def test_basic(self):
config = {}
result = set_irc_server_config(config, "irc.test.com")
assert result["server"] == "irc.test.com"
assert result["port"] == 6697
assert result["use_tls"] is True
def test_custom_port(self):
result = set_irc_server_config({}, "irc.test.com", port=7000, use_tls=False)
assert result["port"] == 7000
assert result["use_tls"] is False
class TestSetIrcNickConfig:
def test_basic(self):
result = set_irc_nick_config({}, "mybot")
assert result["nick"] == "mybot"
assert "username" not in result
def test_with_username(self):
result = set_irc_nick_config({}, "mybot", username="myuser")
assert result["username"] == "myuser"
def test_with_realname(self):
result = set_irc_nick_config({}, "mybot", realname="My IRC Bot")
assert result["realname"] == "My IRC Bot"
class TestSetIrcAuthConfig:
def test_none_auth(self):
result = set_irc_auth_config({}, auth_type="none")
assert result.get("use_sasl") is False
assert "nickserv_enabled" not in result
def test_sasl_auth(self):
result = set_irc_auth_config(
{}, auth_type="sasl", sasl_username="user", sasl_password="pass"
)
assert result["use_sasl"] is True
assert result["sasl_username"] == "user"
assert result["sasl_password"] == "pass"
def test_nickserv_auth(self):
result = set_irc_auth_config(
{}, auth_type="nickserv", nickserv_password="ns_pass"
)
assert result["use_sasl"] is False
assert result["nickserv_enabled"] is True
assert result["nickserv_password"] == "ns_pass"
def test_with_server_password(self):
result = set_irc_auth_config({}, password="serverpass")
assert result["password"] == "serverpass"
# ========================================================================
# setup_entry.py
# ========================================================================
class TestSetupEntry:
def test_get_setup_entry(self):
entry = get_setup_entry()
assert entry["channel_id"] == "irc"
assert entry["label"] == "IRC"
assert len(entry["steps"]) >= 4
def test_register_bundled_channel_setup(self):
entry = register_bundled_channel_setup()
assert entry == get_setup_entry()
# ========================================================================
# commands.py
# ========================================================================
class TestShouldHandleTextCommands:
def test_enabled_with_command(self):
config = {"commands": {"enabled": True}}
assert should_handle_text_commands("!help", config) is True
def test_enabled_with_dot_command(self):
config = {"commands": {"enabled": True}}
assert should_handle_text_commands(".ping", config) is True
def test_disabled(self):
config = {"commands": {"enabled": False}}
assert should_handle_text_commands("!help", config) is False
def test_not_a_command(self):
config = {"commands": {"enabled": True}}
assert should_handle_text_commands("hello", config) is False
def test_whitespace_before_command(self):
config = {"commands": {"enabled": True}}
assert should_handle_text_commands(" !help", config) is True
class TestHasControlCommand:
def test_exclamation(self):
config = {"commands": {"enabled": True}}
assert has_control_command("!help", config) is True
def test_dot(self):
config = {"commands": {"enabled": True}}
assert has_control_command(".ping", config) is True
def test_plain_text(self):
config = {"commands": {"enabled": True}}
assert has_control_command("hello", config) is False
def test_disabled_commands(self):
config = {"commands": {"enabled": False}}
assert has_control_command("!help", config) is False
class TestParseCommand:
def test_simple(self):
cmd, args = parse_command("!help")
assert cmd == "help"
assert args == []
def test_with_args(self):
cmd, args = parse_command("!echo hello world")
assert cmd == "echo"
assert args == ["hello", "world"]
def test_dot_command(self):
cmd, args = parse_command(".ping")
assert cmd == "ping"
class TestCheckCommandAccess:
def test_no_access_groups(self):
config = {"commands": {"enabled": True, "useAccessGroups": False}}
assert check_command_access("alice", "ping", config) is True
def test_no_acl_for_command(self):
config = {"commands": {"enabled": True, "useAccessGroups": True}}
assert check_command_access("alice", "help", config) is True
def test_acl_allows_user(self):
config = {
"commands": {
"enabled": True,
"useAccessGroups": True,
"acl": {"ping": ["admins"]},
}
}
access_groups = {"admins": ["alice"]}
assert check_command_access("alice", "ping", config, access_groups) is True
def test_acl_denies_user(self):
config = {
"commands": {
"enabled": True,
"useAccessGroups": True,
"acl": {"ping": ["admins"]},
}
}
assert check_command_access("bob", "ping", config, {"admins": []}) is False
def test_wildcard_acl(self):
config = {
"commands": {
"enabled": True,
"useAccessGroups": True,
"acl": {"*": ["admins"]},
}
}
assert check_command_access("alice", "custom", config, {"admins": ["alice"]}) is True
class TestHandleCommand:
@pytest.mark.asyncio
async def test_help(self):
send_fn = MagicMock()
response = await handle_command("help", [], "user", "#test", {}, send_fn)
assert "Available commands" in response
@pytest.mark.asyncio
async def test_ping(self):
response = await handle_command("ping", [], "user", "#test", {}, MagicMock())
assert response == "Pong!"
@pytest.mark.asyncio
async def test_status(self):
response = await handle_command("status", [], "user", "#test", {}, MagicMock())
assert "running" in response
@pytest.mark.asyncio
async def test_about(self):
response = await handle_command("about", [], "user", "#test", {}, MagicMock())
assert "ForcePilot" in response
@pytest.mark.asyncio
async def test_unknown_command(self):
response = await handle_command("nonexistent", [], "user", "#test", {}, MagicMock())
assert response is None
@pytest.mark.asyncio
async def test_custom_command_string(self):
config = {"commands": {"custom": {"hello": "Hello, world!"}}}
response = await handle_command("hello", [], "user", "#test", config, MagicMock())
assert response == "Hello, world!"
@pytest.mark.asyncio
async def test_custom_command_dict(self):
config = {"commands": {"custom": {"info": {"response": "Bot Info v1.0"}}}}
response = await handle_command("info", [], "user", "#test", config, MagicMock())
assert response == "Bot Info v1.0"
@pytest.mark.asyncio
async def test_help_lists_custom_commands(self):
config = {"commands": {"custom": {"hello": "hi"}}}
response = await handle_command("help", [], "user", "#test", config, MagicMock())
assert "hello" in response
# ========================================================================
# security.py - IRCSecurityConfig
# ========================================================================
class TestSecurityCheckResult:
def test_default(self):
result = SecurityCheckResult(allowed=True, reason="test")
assert result.allowed is True
assert result.reason == "test"
assert result.challenge_id is None
class TestIRCSecurityConfigCheckDM:
def test_open_policy(self):
config = IRCSecurityConfig(dm_policy="open")
result = config.check_dm("prefix", "alice")
assert result.allowed is True
assert "open" in result.reason
def test_disabled_policy(self):
config = IRCSecurityConfig(dm_policy="disabled")
result = config.check_dm("prefix", "alice")
assert result.allowed is False
def test_pairing_no_allowlist(self):
config = IRCSecurityConfig(dm_policy="pairing")
result = config.check_dm("prefix", "alice")
assert result.allowed is False
assert result.challenge_id is not None
def test_pairing_with_match(self):
config = IRCSecurityConfig(
dm_policy="pairing", allow_from=["alice!user@host"]
)
result = config.check_dm("alice!user@host", "alice")
assert result.allowed is True
def test_open_policy_allows_anyone(self):
config = IRCSecurityConfig(dm_policy="open", allow_from=["alice"])
result = config.check_dm("prefix", "bob")
assert result.allowed is True
assert "open" in result.reason
def test_dangerously_allow_name_matching(self):
config = IRCSecurityConfig(
dm_policy="pairing",
allow_from=["alice"],
dangerously_allow_name_matching=True,
)
result = config.check_dm("alice!user@host", "alice")
assert result.allowed is True
class TestIRCSecurityConfigCheckGroup:
def test_open_policy(self):
config = IRCSecurityConfig(group_policy="open")
result, _resolved = config.check_group("#test", "anyone!x@y", "anyone")
assert result.allowed is True
def test_disabled_policy(self):
config = IRCSecurityConfig(group_policy="disabled")
result, _resolved = config.check_group("#test", "user!x@y", "user")
assert result.allowed is False
def test_allowlist_match(self):
config = IRCSecurityConfig(group_allow_from=["trusted!user@host"])
result, _resolved = config.check_group("#test", "trusted!user@host", "trusted")
assert result.allowed is True
def test_allowlist_no_match(self):
config = IRCSecurityConfig(group_allow_from=["trusted!user@host"])
result, _resolved = config.check_group("#test", "untrusted!x@y", "untrusted")
assert result.allowed is False
def test_channel_disabled(self):
config = IRCSecurityConfig(
groups={"#test": {"enabled": False}, "#other": {}}
)
result, _resolved = config.check_group("#test", "user!x@y", "user")
assert result.allowed is False
def test_channel_specific_allowlist(self):
config = IRCSecurityConfig(
groups={"#test": {"allowFrom": ["channel_user!x@y"]}}
)
result, _resolved = config.check_group("#test", "channel_user!x@y", "channel_user")
assert result.allowed is True
def test_fallback_to_allow_from(self):
config = IRCSecurityConfig(
allow_from=["global!user@host"],
group_allow_from_fallback_to_allow_from=True,
)
result, _resolved = config.check_group("#test", "global!user@host", "global")
assert result.allowed is True
class TestIRCSecurityConfigCheckMention:
def test_mentioned(self):
config = IRCSecurityConfig()
assert config.check_mention("hey mybot help", "mybot") is True
def test_not_mentioned(self):
config = IRCSecurityConfig()
assert config.check_mention("hello everyone", "mybot") is False
def test_require_mention_false(self):
config = IRCSecurityConfig()
resolved = {"requireMention": False}
assert config.check_mention("random text", "mybot", resolved_config=resolved) is True
class TestCollectSecurityWarnings:
def test_tls_warning(self):
warnings = collect_security_warnings(None, use_tls=False)
assert any("TLS" in w for w in warnings)
def test_nickserv_warning(self):
warnings = collect_security_warnings(None, nickserv_enabled=False)
assert any("NickServ" in w for w in warnings)
def test_open_group_policy(self):
config = IRCSecurityConfig(group_policy="open")
warnings = collect_security_warnings(config)
assert any("open" in w.lower() for w in warnings)
def test_mutable_allowlist_entry(self):
config = IRCSecurityConfig(allow_from=["plainnick"])
warnings = collect_security_warnings(config)
assert any("Mutable" in w for w in warnings)
# ========================================================================
# send_cache.py
# ========================================================================
class TestSentMessageEntry:
def test_creation(self):
entry = SentMessageEntry(
message_id="msg1", chat_id="#test", text="hello", sent_at=12345.0
)
assert entry.message_id == "msg1"
assert entry.status == "sent"
class TestSentMessageCache:
def test_put_and_get(self):
cache = SentMessageCache()
cache.put("msg1", "#test", "hello")
entry = cache.get("msg1")
assert entry is not None
assert entry.text == "hello"
def test_get_nonexistent(self):
cache = SentMessageCache()
assert cache.get("nonexistent") is None
def test_update_status(self):
cache = SentMessageCache()
cache.put("msg1", "#test", "hello")
assert cache.update_status("msg1", "delivered") is True
assert cache.get("msg1").status == "delivered"
def test_update_status_nonexistent(self):
cache = SentMessageCache()
assert cache.update_status("nonexistent", "delivered") is False
def test_remove(self):
cache = SentMessageCache()
cache.put("msg1", "#test", "hello")
assert cache.remove("msg1") is True
assert cache.get("msg1") is None
def test_remove_nonexistent(self):
cache = SentMessageCache()
assert cache.remove("nonexistent") is False
def test_size(self):
cache = SentMessageCache()
cache.put("a", "#t", "x")
cache.put("b", "#t", "y")
assert cache.size() == 2
def test_clear(self):
cache = SentMessageCache()
cache.put("a", "#t", "x")
cache.clear()
assert cache.size() == 0
def test_eviction(self):
cache = SentMessageCache(max_entries=3)
for i in range(5):
cache.put(f"msg{i}", "#test", f"text{i}")
assert cache.size() <= 3
def test_ttl_expired(self):
cache = SentMessageCache()
cache.put("msg1", "#test", "hello")
entry = cache._entries["msg1"]
entry.sent_at = time.monotonic() - cache.TTL_SECONDS - 10
assert cache.get("msg1") is None
def test_put_with_metadata(self):
cache = SentMessageCache()
cache.put("msg1", "#test", "hello", metadata={"key": "val"})
entry = cache.get("msg1")
assert entry.metadata == {"key": "val"}
# ========================================================================
# formatter.py
# ========================================================================
class TestFormatOutbound:
def test_basic(self):
identity = ChannelIdentity(
channel_id="irc_test",
channel_type=ChannelType.IRC,
channel_user_id="alice",
channel_chat_id="#test",
)
response = ChannelResponse(identity=identity, content="hello world")
result = format_outbound(response)
assert result["target"] == "#test"
assert result["text"] == "hello world"
class TestConvertMarkdownTables:
def test_off_mode_passes_through(self):
text = "| a | b |\n| - | - |\n| 1 | 2 |"
result = convert_markdown_tables(text, table_mode="off")
assert result == text
def test_plain_mode_renders(self):
text = "| Name | Age |\n|------|-----|\n| Alice| 30 |"
result = convert_markdown_tables(text, table_mode="plain")
assert "Name" in result
assert "Age" in result
def test_list_mode_renders(self):
text = "| Name | Age |\n|------|-----|\n| Alice| 30 |"
result = convert_markdown_tables(text, table_mode="list")
assert "Name:" in result
assert "Age:" in result
def test_no_tables(self):
result = convert_markdown_tables("plain text", table_mode="plain")
assert result == "plain text"
def test_text_with_table_and_plain(self):
text = "Before\n| a | b |\n| - | - |\n| 1 | 2 |\nAfter"
result = convert_markdown_tables(text, table_mode="plain")
assert "Before" in result
assert "After" in result
def test_only_header_no_data(self):
text = "Before\n| a | b |\nAfter"
result = convert_markdown_tables(text, table_mode="plain")
assert "a" in result
# ========================================================================
# nickserv.py
# ========================================================================
class TestNickservFunctions:
def test_identify(self):
result = nickserv_identify("mypass")
assert result == "PRIVMSG NickServ :IDENTIFY mypass"
def test_ghost(self):
result = nickserv_ghost("mybot", "mypass")
assert result == "PRIVMSG NickServ :GHOST mybot mypass"
def test_recover(self):
result = nickserv_recover("mybot", "mypass")
assert result == "PRIVMSG NickServ :RECOVER mybot mypass"
def test_release(self):
result = nickserv_release("mybot", "mypass")
assert result == "PRIVMSG NickServ :RELEASE mybot mypass"
def test_info(self):
result = nickserv_info("mybot")
assert result == "PRIVMSG NickServ :INFO mybot"
def test_status(self):
result = nickserv_status("mybot")
assert result == "PRIVMSG NickServ :STATUS mybot"
# ========================================================================
# media_vision.py
# ========================================================================
class TestExtractMediaUrls:
def test_png(self):
urls = extract_media_urls("check this https://example.com/img.png")
assert len(urls) == 1
assert urls[0] == "https://example.com/img.png"
def test_multiple_formats(self):
text = "https://a.com/1.jpg and https://b.com/2.gif"
urls = extract_media_urls(text)
assert len(urls) == 2
def test_query_params(self):
urls = extract_media_urls("https://example.com/img.png?w=100&h=200")
assert len(urls) == 1
def test_no_media_urls(self):
urls = extract_media_urls("hello world")
assert urls == []
def test_webp_and_svg(self):
urls = extract_media_urls("https://a.com/x.webp https://b.com/y.svg")
assert len(urls) == 2
def test_jpeg_extension(self):
urls = extract_media_urls("https://example.com/photo.jpeg")
assert len(urls) == 1
def test_ignore_non_media(self):
urls = extract_media_urls("https://example.com/page.html")
assert urls == []
class TestHasMediaUrl:
def test_true(self):
assert has_media_url("look at https://example.com/img.png") is True
def test_false(self):
assert has_media_url("no media here") is False
def test_false_for_html(self):
assert has_media_url("https://example.com/page.html") is False
class TestAnalyzeUrls:
def test_single_image(self):
urls = analyze_urls("https://example.com/photo.png")
assert len(urls) == 1
assert urls[0]["type"] == "image"
assert urls[0]["format"] == "png"
assert urls[0]["size_bytes"] is None
def test_extension_normalization(self):
urls = analyze_urls("https://example.com/img.JPG?q=80")
assert urls[0]["format"] == "jpg"
# ========================================================================
# protocol.py 补充
# ========================================================================
class TestParseIrcTagsDict:
def test_with_tags(self):
tags = parse_irc_tags_dict("@msgid=123;time=2024 PRIVMSG #chan :hi")
assert tags == {"msgid": "123", "time": "2024"}
def test_no_tags(self):
tags = parse_irc_tags_dict("PRIVMSG #chan :hi")
assert tags == {}
class TestExtractAccountFromTags:
def test_account_present(self):
assert extract_account_from_tags({"account": "alice"}) == "alice"
def test_account_missing(self):
assert extract_account_from_tags({"other": "val"}) is None
def test_empty_dict(self):
assert extract_account_from_tags({}) is None
class TestIsIdentified:
def test_identified(self):
assert is_identified({"account": "alice"}) is True
def test_not_identified_star(self):
assert is_identified({"account": "*"}) is False
def test_not_identified_missing(self):
assert is_identified({}) is False
# ========================================================================
# channel_runtime.py 补充
# ========================================================================
class TestMergeModes:
def test_empty_current(self):
assert _merge_modes("", "+im") == "+im"
def test_empty_incoming(self):
assert _merge_modes("+nt", "") == "+nt"
def test_add_modes(self):
result = _merge_modes("+nt", "+im")
assert "i" in result
assert "m" in result
def test_remove_modes(self):
result = _merge_modes("+nt", "-t")
assert "t" not in result
assert "n" in result
def test_add_and_remove(self):
result = _merge_modes("+nt", "+im-t")
assert "i" in result
assert "m" in result
assert "t" not in result
assert "n" in result
class TestChannelRuntimeExtended:
def test_handle_quit_removes_from_all_channels(self):
rt = ChannelRuntime()
rt.add_channel("#a")
rt.add_channel("#b")
rt.handle_names("#a", "alice bob")
rt.handle_names("#b", "alice charlie")
rt.handle_quit("alice")
assert "alice" not in rt.get_members("#a")
assert "alice" not in rt.get_members("#b")
assert "bob" in rt.get_members("#a")
def test_handle_nick_change(self):
rt = ChannelRuntime()
rt.add_channel("#test")
rt.handle_names("#test", "alice bob")
rt.handle_nick_change("alice", "alice2")
assert "alice" not in rt.get_members("#test")
assert "alice2" in rt.get_members("#test")
assert "bob" in rt.get_members("#test")
def test_handle_mode(self):
rt = ChannelRuntime()
rt.add_channel("#test")
rt.handle_mode("#test", "+im")
assert "i" in rt.get_modes("#test")
def test_topic(self):
rt = ChannelRuntime()
rt.set_topic("#test", "Welcome!")
assert rt.get_topic("#test") == "Welcome!"
def test_get_topic_nonexistent(self):
rt = ChannelRuntime()
assert rt.get_topic("#nonexistent") is None
def test_mixed_prefix_names(self):
rt = ChannelRuntime()
rt.add_channel("#test")
rt.handle_names("#test", "@op +voice ~owner normal")
members = rt.get_members("#test")
assert "op" in members
assert "voice" in members
assert "owner" in members
assert "normal" in members
def test_handle_names_empty_string(self):
rt = ChannelRuntime()
rt.add_channel("#test")
rt.handle_names("#test", "")
assert rt.get_members("#test") == set()
def test_names_with_only_prefixes(self):
rt = ChannelRuntime()
rt.add_channel("#test")
rt.handle_names("#test", "@ +")
assert rt.get_members("#test") == set()
# ========================================================================
# splits.py 补充
# ========================================================================
class TestSplitMarkdown:
def test_short_paragraph(self):
chunks = split_markdown("hello", 500)
assert chunks == ["hello"]
def test_paragraphs_preserved(self):
text = "para1\n\npara2"
chunks = split_markdown(text, 500)
assert len(chunks) == 2
assert chunks[0] == "para1"
assert chunks[1] == "para2"
def test_long_word_splits(self):
long_word = "A" * 200
chunks = split_markdown(long_word, 50)
for chunk in chunks:
assert len(chunk.encode("utf-8")) <= 50
def test_empty_text(self):
assert split_markdown("", 500) == [""]
def test_zero_limit(self):
assert split_markdown("hello", 0) == ["hello"]
class TestSplitIrcTextExtended:
def test_custom_max_chars(self):
result = split_irc_text("A" * 500, max_chars=50)
assert len(result) > 1
def test_default_max_chars(self):
short = "hello"
result = split_irc_text(short)
assert result == ["hello"]
# ========================================================================
# auth.py 补充
# ========================================================================
class TestResolvePassword:
def test_direct_password(self):
result = resolve_password(password="directpass")
assert result == "directpass"
def test_password_file(self, tmp_path):
pw_file = tmp_path / "password.txt"
pw_file.write_text("filepass")
result = resolve_password(password_file=str(pw_file))
assert result == "filepass"
def test_password_file_priority(self, tmp_path):
pw_file = tmp_path / "password.txt"
pw_file.write_text("filepass")
result = resolve_password(password="inline", password_file=str(pw_file))
assert result == "filepass"
def test_env_var(self, monkeypatch):
monkeypatch.setenv("IRC_TEST_PASS", "envpass")
result = resolve_password(env_var="IRC_TEST_PASS")
assert result == "envpass"
def test_env_var_fallback(self, monkeypatch):
monkeypatch.setenv("IRC_TEST_PASS", "envpass")
result = resolve_password(password="", password_file="", env_var="IRC_TEST_PASS")
assert result == "envpass"
def test_empty_result(self):
result = resolve_password()
assert result == ""
def test_nonexistent_file(self):
result = resolve_password(password_file="/nonexistent/path")
assert result == ""
class TestGetServerPassword:
def test_from_config(self):
config = {"server_password": "spass"}
assert get_server_password(config) == "spass"
def test_from_file(self, tmp_path):
pw_file = tmp_path / "server_pass.txt"
pw_file.write_text("filepass")
config = {"server_password_file": str(pw_file)}
assert get_server_password(config) == "filepass"
def test_from_env(self, monkeypatch):
monkeypatch.setenv("IRC_SERVER_PASSWORD", "envpass")
assert get_server_password({}) == "envpass"
# ========================================================================
# ctcp.py 补充
# ========================================================================
class TestHandleCtcp:
@pytest.mark.asyncio
async def test_ping_responds(self):
send_fn = MagicMock()
result = await handle_ctcp(send_fn, "alice", "\x01PING 12345\x01")
assert result is False
send_fn.assert_called_once()
assert "PING" in send_fn.call_args[0][0]
@pytest.mark.asyncio
async def test_not_ctcp(self):
send_fn = MagicMock()
result = await handle_ctcp(send_fn, "alice", "plain text")
assert result is False
send_fn.assert_not_called()
@pytest.mark.asyncio
async def test_action_returns_true(self):
send_fn = MagicMock()
result = await handle_ctcp(send_fn, "alice", "\x01ACTION waves\x01")
assert result is True
@pytest.mark.asyncio
async def test_version_responds(self):
send_fn = MagicMock()
await handle_ctcp(send_fn, "alice", "\x01VERSION\x01")
assert any("ForcePilot" in c[0][0] for c in send_fn.call_args_list)
@pytest.mark.asyncio
async def test_time_responds(self):
send_fn = MagicMock()
await handle_ctcp(send_fn, "alice", "\x01TIME\x01")
assert any("TIME " in c[0][0] for c in send_fn.call_args_list)
class TestExtractCtcpAction:
def test_action_extracted(self):
action = extract_ctcp_action("\x01ACTION waves hello\x01")
assert action == "waves hello"
def test_not_action(self):
assert extract_ctcp_action("\x01PING 123\x01") is None
def test_not_ctcp(self):
assert extract_ctcp_action("plain text") is None
class TestSupportsCtcp:
def test_returns_true(self):
assert supports_ctcp() is True
# ========================================================================
# config_ui_hints.py
# ========================================================================
class TestGetConfigUiHints:
def test_returns_dict_with_expected_keys(self):
hints = get_config_ui_hints()
assert isinstance(hints, dict)
assert "server" in hints
assert "nick" in hints
assert "port" in hints
assert "use_tls" in hints
assert "dmPolicy" in hints
def test_server_hints_structure(self):
hints = get_config_ui_hints()
server = hints["server"]
assert server["type"] == "text"
assert server["required"] is True
assert server["placeholder"] == "irc.libera.chat"
def test_password_fields_are_sensitive(self):
hints = get_config_ui_hints()
for key in ["nickserv.password", "password", "sasl.password"]:
if key in hints:
assert hints[key]["sensitive"] is True, f"{key} should be sensitive"
def test_dm_policy_options(self):
hints = get_config_ui_hints()
dm = hints["dmPolicy"]
assert dm["type"] == "select"
options = [o["value"] for o in dm["options"]]
assert "pairing" in options
assert "open" in options
assert "disabled" in options
def test_chunker_mode_options(self):
hints = get_config_ui_hints()
chunker = hints["chunker_mode"]
options = [o["value"] for o in chunker["options"]]
assert "length" in options
assert "markdown" in options
# ========================================================================
# 边界条件与异常处理综合测试
# ========================================================================
class TestEdgeCases:
def test_parse_irc_tags_malformed(self):
tags = parse_irc_tags("@msgid PING")
assert tags == {"msgid": True}
def test_parse_irc_tags_edge_at_only(self):
tags = parse_irc_tags("@ PING")
assert tags is not None
def test_resolve_allowlist_match_partial_host(self):
result = resolve_allowlist_match(["nick@host.com"], ["*@host*"])
assert result == "*@host*"
def test_build_allowlist_candidates_unicode(self):
candidates = build_allowlist_candidates("昵称测试", "user", "host")
assert "昵称测试" in candidates
assert "昵称测试!user@host" in candidates
def test_directory_empty_config(self):
assert list_users({}) == []
assert list_groups({}) == []
def test_secret_contract_all_keys_unique(self):
keys = [t.key for t in IRC_SECRET_TARGETS]
assert len(keys) == len(set(keys))
def test_known_servers_all_have_tls(self):
for _name, details in KNOWN_IRC_SERVERS.items():
assert details["tls"] is True, f"{_name} should have TLS enabled"
def test_commands_parse_nested_args(self):
cmd, args = parse_command("!cmd a b c d e")
assert cmd == "cmd"
assert len(args) == 5
def test_send_cache_max_entries_custom(self):
cache = SentMessageCache(max_entries=2)
cache.put("a", "#t", "x")
cache.put("b", "#t", "y")
cache.put("c", "#t", "z")
assert cache.size() == 2
def test_convert_markdown_tables_no_separator_still_rendered(self):
text = "| Name |\n| Alice |"
result = convert_markdown_tables(text, table_mode="plain")
assert "Name" in result