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

468 lines
15 KiB
Python
Raw Normal View History

"""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