mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-08 03:30:34 +00:00
* fix(channels): key WeCom group conversations by chatid, not the sender Group messages used the sender's userid as chat_id and topic_id, the same key as the sender's private chat. Wherever no group connection kept them apart (auth-disabled mode, require_bound_identity: false, legacy group bindings), a member's private chat and every group they wrote in ran in one DeerFlow thread. Groups are now keyed by chatid with one thread per member. A group-scoped connection carries its sender-keyed thread over once; a legacy binding, which spans every group of the bot, does not. * docs(changelog): reference #7794 for the WeCom group conversation fix --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
69 lines
3.6 KiB
Python
69 lines
3.6 KiB
Python
"""WeCom group binding names and legacy identity boundaries, without provider I/O."""
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
from app.channels.message_bus import InboundMessage, MessageBus
|
|
from app.channels.wecom import WeComChannel
|
|
from deerflow.persistence.channel_connections import ChannelConnectionRepository
|
|
from deerflow.persistence.engine import close_engine, get_session_factory, init_engine
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def repo(tmp_path):
|
|
await init_engine("sqlite", url=f"sqlite+aiosqlite:///{tmp_path / 'wecom.db'}", sqlite_dir=str(tmp_path))
|
|
try:
|
|
yield ChannelConnectionRepository(get_session_factory())
|
|
finally:
|
|
await close_engine()
|
|
|
|
|
|
def inbound(group="wr-group", bot="bot-1"):
|
|
return InboundMessage(channel_name="wecom", chat_id=group, user_id="alice", text="group follow-up", topic_id="alice", metadata={"aibotid": bot, "chattype": "group", "chatid": group})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("private_metadata", [{"aibotid": "bot-1", "chattype": "single"}, {"aibotid": "bot-1"}, {}])
|
|
async def test_private_binding_does_not_authorize_an_unbound_group(repo, private_metadata):
|
|
await repo.upsert_connection(owner_user_id="owner", provider="wecom", external_account_id="alice", workspace_id="bot-1", metadata=private_metadata)
|
|
channel = WeComChannel(MessageBus(), {"connection_repo": repo})
|
|
message = await channel._attach_connection_identity(inbound())
|
|
assert message.connection_id is None
|
|
assert message.owner_user_id is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_legacy_group_binding_retains_owner(repo):
|
|
# The thread it had under the sender key is not continued; see
|
|
# test_wecom_conversation_keys.py.
|
|
legacy = await repo.upsert_connection(owner_user_id="owner", provider="wecom", external_account_id="alice", workspace_id="bot-1", metadata={"aibotid": "bot-1", "chattype": "group"})
|
|
channel = WeComChannel(MessageBus(), {"connection_repo": repo})
|
|
message = await channel._attach_connection_identity(inbound())
|
|
assert message.connection_id == legacy["id"]
|
|
assert message.owner_user_id == "owner"
|
|
other_bot = await channel._attach_connection_identity(inbound(bot="bot-2"))
|
|
assert other_bot.connection_id is None
|
|
await repo.disconnect_connection(connection_id=legacy["id"], owner_user_id="owner")
|
|
revoked = await channel._attach_connection_identity(inbound())
|
|
assert revoked.connection_id is None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_named_group_connect_records_a_human_label_without_exposing_chatid(repo):
|
|
channel = WeComChannel(MessageBus(), {"connection_repo": repo})
|
|
channel._ws_client = SimpleNamespace(reply=AsyncMock(), reply_stream=AsyncMock())
|
|
for code, group, label in (("code-1", "wr-group-1", "Release team"), ("code-2", "wr-group-2", "Support team")):
|
|
await repo.create_oauth_state(owner_user_id="owner", provider="wecom", state=code, expires_at=datetime.now(UTC) + timedelta(minutes=5), metadata={"notification_group_name": label})
|
|
await channel._publish_ws_inbound(
|
|
{"body": {"aibotid": "bot-1", "chattype": "group", "chatid": group, "msgid": f"msg-{code}", "from": {"userid": "alice"}, "text": {"content": f"/connect {code}"}}},
|
|
f"/connect {code}",
|
|
)
|
|
rows = await repo.list_connections("owner")
|
|
assert len(rows) == 2
|
|
assert {row["workspace_name"] for row in rows} == {"Release team", "Support team"}
|
|
assert {row["workspace_id"] for row in rows} == {"bot-1:group:wr-group-1", "bot-1:group:wr-group-2"}
|