"""MSTeams chunking / commands / command_gate 单元测试。""" from __future__ import annotations import pytest from yuxi.channels.adapters.msteams.chunking import chunk_text from yuxi.channels.adapters.msteams.commands import extract_command, build_command_help_card from yuxi.channels.adapters.msteams.command_gate import CommandGate class TestChunkText: def test_short_text_returns_single_chunk(self): result = chunk_text("hello", limit=4000) assert result == ["hello"] def test_exact_limit_text_returns_single_chunk(self): text = "x" * 100 result = chunk_text(text, limit=100) assert result == [text] def test_length_mode_short(self): result = chunk_text("hi", limit=10, mode="length") assert result == ["hi"] def test_length_mode_splits_evenly(self): text = "abcdefghij" result = chunk_text(text, limit=3, mode="length") assert len(result) == 4 assert "".join(result) == text def test_length_mode_exact_chunks(self): text = "abcabc" result = chunk_text(text, limit=3, mode="length") assert result == ["abc", "abc"] def test_newline_mode_basic(self): text = "line1\nline2\nline3" result = chunk_text(text, limit=20, mode="newline") assert result == ["line1\nline2\nline3"] def test_newline_mode_splits_long_lines(self): text = "\n".join(["A" * 10, "B" * 10, "C" * 10]) result = chunk_text(text, limit=15, mode="newline") assert len(result) == 3 def test_newline_mode_merges_short_lines(self): text = "\n".join(["a", "b", "c"]) result = chunk_text(text, limit=100, mode="newline") assert len(result) == 1 assert "a\nb\nc" in result[0] def test_paragraph_mode_basic(self): text = "Short paragraph." result = chunk_text(text, limit=100) assert len(result) == 1 assert "Short paragraph." in result[0] def test_paragraph_mode_splits_long(self): text = "Sentence A. Sentence B. Sentence C." result = chunk_text(text, limit=20) assert len(result) > 1 def test_paragraph_mode_multi_paragraph(self): text = "Para1.\n\nPara2." result = chunk_text(text, limit=10) assert len(result) >= 2 def test_empty_text(self): result = chunk_text("", limit=100) assert result == [""] def test_default_mode_is_paragraph(self): text = "A" * 5000 result = chunk_text(text, limit=4000) assert len(result) >= 1 def test_paragraph_merges_small_chunks(self): text = "a.\n\nb." result = chunk_text(text, limit=200) assert len(result) == 1 class TestExtractCommand: def test_valid_command(self): cmd, args = extract_command("/help") assert cmd == "/help" assert args == "" def test_valid_command_with_args(self): cmd, args = extract_command("/reset confirm") assert cmd == "/reset" assert args == "confirm" def test_unknown_command(self): cmd, content = extract_command("/unknown") assert cmd is None assert content == "/unknown" def test_non_command_text(self): cmd, content = extract_command("hello world") assert cmd is None assert content == "hello world" def test_empty_text(self): cmd, content = extract_command("") assert cmd is None assert content == "" def test_command_case_insensitive(self): cmd, args = extract_command("/HELP") assert cmd == "/help" def test_command_with_extra_whitespace(self): cmd, args = extract_command(" /help test ") assert cmd == "/help" assert args == "test" def test_all_known_commands(self): for cmd_name in ["/reset", "/history", "/context", "/summary", "/help", "/status"]: cmd, _ = extract_command(cmd_name) assert cmd == cmd_name class TestBuildCommandHelpCard: def test_returns_adaptive_card(self): card = build_command_help_card() assert card["type"] == "AdaptiveCard" assert card["version"] == "1.5" def test_card_contains_fact_set(self): card = build_command_help_card() body_types = [b["type"] for b in card["body"]] assert "FactSet" in body_types class TestCommandGateBasic: @pytest.fixture def gate(self): return CommandGate() def test_default_command_enabled_is_true(self, gate): assert gate._command_enabled is True def test_basic_command_authorized_direct(self, gate): assert gate.is_command_authorized("help", "direct") is True def test_basic_command_authorized_group(self, gate): assert gate.is_command_authorized("status", "group") is True def test_command_normalized(self, gate): assert gate.is_command_authorized("/help", "direct") is True def test_commands_disabled(self): gate = CommandGate({"command_enabled": False}) assert gate.is_command_authorized("help", "direct") is False assert gate.is_command_authorized("help", "group") is False class TestCommandGateDM: def test_dm_blocked_command(self): gate = CommandGate({"dm_blocked_commands": ["reset"]}) assert gate.is_command_authorized("reset", "direct") is False assert gate.is_command_authorized("help", "direct") is True def test_dm_allowed_only(self): gate = CommandGate({"dm_allowed_commands": ["help", "status"]}) assert gate.is_command_authorized("help", "direct") is True assert gate.is_command_authorized("status", "direct") is True assert gate.is_command_authorized("reset", "direct") is False def test_dm_blocked_takes_priority(self): gate = CommandGate({ "dm_allowed_commands": ["help", "reset"], "dm_blocked_commands": ["reset"], }) assert gate.is_command_authorized("help", "direct") is True assert gate.is_command_authorized("reset", "direct") is False class TestCommandGateGroup: def test_group_blocked_command(self): gate = CommandGate({"group_blocked_commands": ["summary"]}) assert gate.is_command_authorized("summary", "group") is False assert gate.is_command_authorized("help", "group") is True def test_channel_uses_group_policy(self): gate = CommandGate({"group_blocked_commands": ["summary"]}) assert gate.is_command_authorized("summary", "channel") is False def test_group_allowed_only(self): gate = CommandGate({"group_allowed_commands": ["help"]}) assert gate.is_command_authorized("help", "group") is True assert gate.is_command_authorized("status", "group") is False class TestCommandGateAdmin: def test_admin_with_empty_admin_ids(self): gate = CommandGate() assert gate.is_admin("any-user") is False def test_admin_recognized(self): gate = CommandGate({"admin_user_ids": ["admin-001", "admin-002"]}) assert gate.is_admin("admin-001") is True assert gate.is_admin("admin-002") is True assert gate.is_admin("user-001") is False class TestCommandGateResolve: def test_resolve_with_team_config(self): gate = CommandGate({"dm_allowed_commands": ["help"]}) team_config = {"dm_allowed_commands": ["help", "reset"]} result = gate.resolve_command_gate(team_config, "reset", "direct") assert result is True def test_resolve_falls_back_to_self(self): gate = CommandGate({"dm_allowed_commands": ["help"]}) result = gate.resolve_command_gate(None, "help", "direct") assert result is True def test_resolve_with_no_team_overrides(self): gate = CommandGate({"dm_allowed_commands": ["help"]}) team_config = {"group_allowed_commands": ["status"]} result = gate.resolve_command_gate(team_config, "help", "direct") assert result is True class TestCommandGateDual: def test_dual_gate_dm(self): gate = CommandGate() dm_config = {"dm_allowed_commands": ["help", "reset"]} assert gate.resolve_dual_text_control_command_gate(dm_config, None, "help", "direct") is True assert gate.resolve_dual_text_control_command_gate(dm_config, None, "status", "direct") is False def test_dual_gate_group(self): gate = CommandGate() group_config = {"group_allowed_commands": ["help"]} assert gate.resolve_dual_text_control_command_gate(None, group_config, "help", "group") is True assert gate.resolve_dual_text_control_command_gate(None, group_config, "status", "group") is False def test_dual_gate_both(self): gate = CommandGate() dm_config = {"dm_allowed_commands": ["help"]} group_config = {"group_allowed_commands": ["status"]} assert gate.resolve_dual_text_control_command_gate(dm_config, group_config, "help", "direct") is True assert gate.resolve_dual_text_control_command_gate(dm_config, group_config, "status", "group") is True def test_dual_gate_blocks_disabled(self): gate = CommandGate({"command_enabled": False}) assert gate.resolve_dual_text_control_command_gate(None, None, "help", "direct") is False