"""MSTeams config_resolver / route_config / tool_policy 单元测试。""" from __future__ import annotations import pytest from yuxi.channels.adapters.msteams.config_resolver import ( resolve_nested_config, resolve_reply_style, resolve_require_mention, ) from yuxi.channels.adapters.msteams.route_config import resolve_route_config from yuxi.channels.adapters.msteams.tool_policy import ToolPolicy, resolve_tool_policy class TestResolveNestedConfig: def test_empty_config(self): result = resolve_nested_config({}, "team-1", "ch-1") assert result == {} def test_exact_team_match(self): config = { "teams": { "team-1": {"reply_style": "thread"}, } } result = resolve_nested_config(config, "team-1") assert result["team_config"]["reply_style"] == "thread" def test_wildcard_team_match(self): config = { "teams": { "*": {"reply_style": "thread"}, } } result = resolve_nested_config(config, "unknown-team") assert result["team_config"]["reply_style"] == "thread" def test_no_team_match(self): config = {"teams": {"team-1": {"reply_style": "thread"}}} result = resolve_nested_config(config, "team-2") assert result == {} def test_channel_config_in_team(self): config = { "teams": { "team-1": { "reply_style": "thread", "channels": {"ch-1": {"require_mention": True}}, } } } result = resolve_nested_config(config, "team-1", "ch-1") assert result["team_config"]["reply_style"] == "thread" assert result["channel_config"]["require_mention"] is True def test_channel_wildcard(self): config = { "teams": { "team-1": { "channels": {"*": {"require_mention": True}}, } } } result = resolve_nested_config(config, "team-1", "ch-unknown") assert result["channel_config"]["require_mention"] is True def test_channel_not_found_no_wildcard(self): config = { "teams": { "team-1": { "channels": {"ch-1": {"require_mention": True}}, } } } result = resolve_nested_config(config, "team-1", "ch-2") assert result.get("channel_config") is None def test_no_channel_id(self): config = { "teams": { "team-1": { "reply_style": "thread", "channels": {"ch-1": {}}, } } } result = resolve_nested_config(config, "team-1") assert "channel_config" not in result assert result["team_config"]["reply_style"] == "thread" class TestResolveReplyStyle: def test_channel_override(self): config = { "reply_style": "inline", "teams": { "team-1": { "reply_style": "thread", "channels": {"ch-1": {"reply_style": "new_thread"}}, } }, } result = resolve_reply_style(config, "team-1", "ch-1") assert result == "new_thread" def test_team_override(self): config = { "reply_style": "inline", "teams": {"team-1": {"reply_style": "thread"}}, } result = resolve_reply_style(config, "team-1", "ch-1") assert result == "thread" def test_global_default(self): config = {"reply_style": "inline"} result = resolve_reply_style(config, "team-1") assert result == "inline" def test_fallback_default(self): result = resolve_reply_style({}, "team-1") assert result == "thread" class TestResolveRequireMention: def test_channel_override(self): config = { "teams": { "team-1": { "require_mention": False, "channels": {"ch-1": {"require_mention": True}}, } }, } result = resolve_require_mention(config, "team-1", "ch-1") assert result is True def test_team_override(self): config = { "require_mention": False, "teams": {"team-1": {"require_mention": True}}, } result = resolve_require_mention(config, "team-1") assert result is True def test_global_fallback(self): config = {"require_mention": True} result = resolve_require_mention(config, "team-1") assert result is True def test_none_when_not_configured(self): result = resolve_require_mention({}, "team-1") assert result is None class TestResolveRouteConfig: def test_no_routes_returns_original(self): config = {"app_id": "test"} result = resolve_route_config(config) assert result == config def test_empty_routes(self): config = {"app_id": "test", "routes": []} result = resolve_route_config(config) assert result == config def test_exact_team_id_match(self): config = { "app_id": "global", "routes": [ { "match_team": {"id": "team-1"}, "config": {"app_id": "team-override"}, } ], } result = resolve_route_config(config, team_id="team-1") assert result["app_id"] == "team-override" def test_team_name_match(self): config = { "routes": [ { "match_team": {"name": "My Team"}, "config": {"streaming_mode": "off"}, } ], } result = resolve_route_config(config, team_name="My Team") assert result["streaming_mode"] == "off" def test_team_name_case_insensitive(self): config = { "routes": [ { "match_team": {"name": "My Team"}, "config": {"streaming_mode": "off"}, } ], } result = resolve_route_config(config, team_name="my team") assert result["streaming_mode"] == "off" def test_team_slug_match(self): config = { "routes": [ { "match_team": {"slug": "my-team"}, "config": {"reply_style": "thread"}, } ], } result = resolve_route_config(config, team_name="My Team") assert result["reply_style"] == "thread" def test_slug_no_match(self): config = { "routes": [ { "match_team": {"slug": "other-team"}, "config": {"reply_style": "thread"}, } ], } result = resolve_route_config(config, team_name="My Team") assert "reply_style" not in result def test_team_and_channel_match(self): config = { "routes": [ { "match_team": {"id": "team-1"}, "match_channel": {"id": "ch-1"}, "config": {"require_mention": False}, } ], } result = resolve_route_config(config, team_id="team-1", channel_id="ch-1") assert result["require_mention"] is False def test_channel_mismatch(self): config = { "routes": [ { "match_team": {"id": "team-1"}, "match_channel": {"id": "ch-1"}, "config": {"require_mention": False}, } ], } result = resolve_route_config(config, team_id="team-1", channel_id="ch-2") assert result == config def test_wildcard_route(self): config = { "routes": [ { "match_team": {"id": "*"}, "config": {"reply_style": "thread"}, } ], } result = resolve_route_config(config, team_id="any-team") assert result["reply_style"] == "thread" def test_first_match_wins(self): config = { "routes": [ { "match_team": {"id": "team-1"}, "config": {"reply_style": "first"}, }, { "match_team": {"id": "team-1"}, "config": {"reply_style": "second"}, }, ], } result = resolve_route_config(config, team_id="team-1") assert result["reply_style"] == "first" def test_merged_config_preserves_original(self): config = { "app_id": "test", "routes": [ { "match_team": {"id": "team-1"}, "config": {"reply_style": "thread"}, } ], } result = resolve_route_config(config, team_id="team-1") assert result["app_id"] == "test" assert result["reply_style"] == "thread" class TestToolPolicyBasic: def test_default_allows_all(self): policy = ToolPolicy() assert policy.is_tool_allowed("any_tool") is True assert policy.is_tool_allowed("web_search") is True def test_global_allow_restricts(self): policy = ToolPolicy({"tools_allow": ["web_search", "code_interpreter"]}) assert policy.is_tool_allowed("web_search") is True assert policy.is_tool_allowed("file_upload") is False def test_global_deny_blocks(self): policy = ToolPolicy({"tools_deny": ["web_search"]}) assert policy.is_tool_allowed("web_search") is False assert policy.is_tool_allowed("code_interpreter") is True def test_global_deny_overrides_allow(self): policy = ToolPolicy({ "tools_allow": ["web_search"], "tools_deny": ["web_search"], }) assert policy.is_tool_allowed("web_search") is False class TestToolPolicySender: def test_sender_allow(self): policy = ToolPolicy({ "tools_by_sender": [{"sender_id": "user-1", "allow": ["web_search"]}], }) assert policy.is_tool_allowed("web_search", sender_id="user-1") is True assert policy.is_tool_allowed("code_interpreter", sender_id="user-1") is False def test_sender_deny(self): policy = ToolPolicy({ "tools_by_sender": [{"sender_id": "user-1", "allow": ["web_search", "code"], "deny": ["web_search"]}], }) assert policy.is_tool_allowed("web_search", sender_id="user-1") is False assert policy.is_tool_allowed("code", sender_id="user-1") is True def test_sender_not_matched_falls_back(self): policy = ToolPolicy({ "tools_allow": ["web_search"], "tools_by_sender": [{"sender_id": "user-1", "allow": ["code_interpreter"]}], }) assert policy.is_tool_allowed("web_search", sender_id="user-2") is True assert policy.is_tool_allowed("code_interpreter", sender_id="user-2") is False class TestToolPolicyTeamChannel: def test_team_allow(self): policy = ToolPolicy({ "teams": { "team-1": {"tools": {"allow": ["web_search"]}}, }, }) assert policy.is_tool_allowed("web_search", team_id="team-1") is True assert policy.is_tool_allowed("code_interpreter", team_id="team-1") is False def test_team_deny(self): policy = ToolPolicy({ "teams": { "team-1": {"tools": {"deny": ["web_search"]}}, }, }) assert policy.is_tool_allowed("web_search", team_id="team-1") is False def test_channel_allow_override_team(self): policy = ToolPolicy({ "teams": { "team-1": { "tools": {"allow": ["code"]}, "channels": { "ch-1": {"tools": {"allow": ["web_search"]}}, }, }, }, }) assert policy.is_tool_allowed("web_search", team_id="team-1", channel_id="ch-1") is False assert policy.is_tool_allowed("code", team_id="team-1", channel_id="ch-1") is False class TestToolPolicyGetAllowed: def test_global_allow_list(self): policy = ToolPolicy({"tools_allow": ["tool_a", "tool_b"]}) allowed = policy.get_allowed_tools() assert allowed == ["tool_a", "tool_b"] def test_no_allow_list_returns_none(self): policy = ToolPolicy() assert policy.get_allowed_tools() is None def test_sender_allow(self): policy = ToolPolicy({ "tools_by_sender": [{"sender_id": "user-1", "allow": ["tool_a", "tool_b"]}], }) allowed = policy.get_allowed_tools(sender_id="user-1") assert allowed == ["tool_a", "tool_b"] def test_channel_overrides_team(self): policy = ToolPolicy({ "teams": { "team-1": { "tools": {"allow": ["team_tool"]}, "channels": { "ch-1": {"tools": {"allow": ["channel_tool"]}}, }, }, }, }) allowed = policy.get_allowed_tools(team_id="team-1", channel_id="ch-1") assert allowed == ["channel_tool"] def test_team_fallback_when_no_channel(self): policy = ToolPolicy({ "teams": { "team-1": {"tools": {"allow": ["team_tool"]}}, }, }) allowed = policy.get_allowed_tools(team_id="team-1") assert allowed == ["team_tool"] def test_deny_excludes_from_allow(self): policy = ToolPolicy({ "teams": { "team-1": { "tools": {"allow": ["tool_a", "tool_b"], "deny": ["tool_b"]}, }, }, }) allowed = policy.get_allowed_tools(team_id="team-1") assert "tool_b" not in allowed assert "tool_a" in allowed class TestToolPolicyGetDenied: def test_empty_config_returns_empty(self): policy = ToolPolicy() denied = policy.get_denied_tools() assert denied == set() def test_global_deny(self): policy = ToolPolicy({"tools_deny": ["tool_a", "tool_b"]}) denied = policy.get_denied_tools() assert denied == {"tool_a", "tool_b"} def test_combined_deny(self): policy = ToolPolicy({ "tools_deny": ["global_deny"], "teams": { "team-1": { "tools": {"deny": ["team_deny"]}, "channels": { "ch-1": {"tools": {"deny": ["channel_deny"]}}, }, }, }, }) denied = policy.get_denied_tools(team_id="team-1", channel_id="ch-1") assert "global_deny" in denied assert "team_deny" in denied assert "channel_deny" in denied class TestResolveToolPolicy: def test_allow(self): assert resolve_tool_policy({"tools_allow": ["web_search"]}, "web_search") is True def test_deny(self): assert resolve_tool_policy({"tools_deny": ["web_search"]}, "web_search") is False def test_allow_missing(self): assert resolve_tool_policy({"tools_allow": ["web_search"]}, "code_interpreter") is False