Skip to content

Commit 7c5f391

Browse files
whatevertogoclaude
andcommitted
test: refactor and enhance P2 platform adapter tests
- Simplify test_other_adapters.py using import-only tests - Update webchat, wecom, dingtalk, lark, slack tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 94736ff commit 7c5f391

8 files changed

Lines changed: 1733 additions & 3 deletions

File tree

astrbot/core/platform/sources/webchat/webchat_adapter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def __init__(
6666
support_proactive_message=False,
6767
)
6868
self._shutdown_event = asyncio.Event()
69+
self.stop_event = self._shutdown_event
6970
self._webchat_queue_mgr = webchat_queue_mgr
7071

7172
async def send_by_session(

astrbot/core/platform/sources/webchat/webchat_event.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
import shutil
55
import uuid
66

7-
from astrbot.api import logger
8-
from astrbot.api.event import AstrMessageEvent, MessageChain
9-
from astrbot.api.message_components import File, Image, Json, Plain, Record
7+
from astrbot import logger
8+
from astrbot.core.message.components import File, Image, Json, Plain, Record
9+
from astrbot.core.message.message_event_result import MessageChain
10+
from astrbot.core.platform import AstrMessageEvent
1011
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
1112

1213
from .webchat_queue_mgr import webchat_queue_mgr
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
"""Isolated tests for DingTalk adapter using subprocess + stubbed dingtalk_stream."""
2+
3+
from __future__ import annotations
4+
5+
import subprocess
6+
import sys
7+
import textwrap
8+
from pathlib import Path
9+
10+
11+
def _run_python(code: str) -> subprocess.CompletedProcess[str]:
12+
repo_root = Path(__file__).resolve().parents[2]
13+
return subprocess.run(
14+
[sys.executable, "-c", textwrap.dedent(code)],
15+
cwd=repo_root,
16+
capture_output=True,
17+
text=True,
18+
check=False,
19+
)
20+
21+
22+
def _assert_dingtalk_case(case: str) -> None:
23+
code = f"""
24+
import asyncio
25+
import sys
26+
import threading
27+
import types
28+
29+
case = {case!r}
30+
31+
dingtalk = types.ModuleType("dingtalk_stream")
32+
33+
class EventHandler:
34+
pass
35+
36+
class EventMessage:
37+
pass
38+
39+
class AckMessage:
40+
STATUS_OK = "OK"
41+
42+
class Credential:
43+
def __init__(self, *args, **kwargs):
44+
pass
45+
46+
class ChatbotHandler:
47+
pass
48+
49+
class CallbackMessage:
50+
pass
51+
52+
class ChatbotMessage:
53+
TOPIC = "/v1.0/chatbot/messages"
54+
55+
@staticmethod
56+
def from_dict(data):
57+
return types.SimpleNamespace(
58+
create_at=1700000000000,
59+
conversation_type="1",
60+
sender_id=data.get("sender_id", "user_1"),
61+
sender_nick="Nick",
62+
chatbot_user_id="bot_1",
63+
message_id="msg_1",
64+
at_users=[],
65+
conversation_id=data.get("conversation_id", "conv_1"),
66+
message_type="text",
67+
text=types.SimpleNamespace(content=data.get("text", "hello")),
68+
sender_staff_id=data.get("sender_staff_id", "staff_1"),
69+
robot_code="robot_1",
70+
)
71+
72+
class DummyWS:
73+
def __init__(self):
74+
self.closed = False
75+
76+
async def close(self, code=1000, reason=""):
77+
self.closed = True
78+
79+
class DingTalkStreamClient:
80+
def __init__(self, *args, **kwargs):
81+
self.websocket = None
82+
self.handlers = []
83+
self.callback_handlers = []
84+
self.open_connection = None
85+
86+
def register_all_event_handler(self, handler):
87+
self.handlers.append(handler)
88+
89+
def register_callback_handler(self, topic, handler):
90+
self.callback_handlers.append((topic, handler))
91+
92+
async def start(self):
93+
return None
94+
95+
def get_access_token(self):
96+
return "token"
97+
98+
class RichTextContent:
99+
pass
100+
101+
dingtalk.EventHandler = EventHandler
102+
dingtalk.EventMessage = EventMessage
103+
dingtalk.AckMessage = AckMessage
104+
dingtalk.Credential = Credential
105+
dingtalk.ChatbotHandler = ChatbotHandler
106+
dingtalk.CallbackMessage = CallbackMessage
107+
dingtalk.ChatbotMessage = ChatbotMessage
108+
dingtalk.DingTalkStreamClient = DingTalkStreamClient
109+
dingtalk.RichTextContent = RichTextContent
110+
111+
sys.modules["dingtalk_stream"] = dingtalk
112+
113+
from astrbot.api.message_components import Plain
114+
from astrbot.core.message.message_event_result import MessageChain
115+
from astrbot.core.platform.astr_message_event import MessageSesion
116+
from astrbot.api.platform import MessageType
117+
from astrbot.core.platform.sources.dingtalk.dingtalk_adapter import DingtalkPlatformAdapter
118+
119+
def _cfg():
120+
return {{
121+
"id": "dingtalk_test",
122+
"client_id": "client_id",
123+
"client_secret": "client_secret",
124+
}}
125+
126+
async def _run_async_case():
127+
if case == "send_group":
128+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
129+
called = {{"ok": False}}
130+
131+
async def _send_group(open_conversation_id, robot_code, message_chain):
132+
called["ok"] = True
133+
assert open_conversation_id == "group_1"
134+
assert robot_code == "client_id"
135+
136+
adapter.send_message_chain_to_group = _send_group
137+
session = MessageSesion(
138+
platform_name="dingtalk",
139+
message_type=MessageType.GROUP_MESSAGE,
140+
session_id="group_1",
141+
)
142+
await adapter.send_by_session(session, MessageChain([Plain("hello")]))
143+
assert called["ok"] is True
144+
return
145+
146+
if case == "send_private":
147+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
148+
called = {{"ok": False}}
149+
150+
async def _get_staff(session):
151+
return "staff_99"
152+
153+
async def _send_user(staff_id, robot_code, message_chain):
154+
called["ok"] = True
155+
assert staff_id == "staff_99"
156+
assert robot_code == "client_id"
157+
158+
adapter._get_sender_staff_id = _get_staff
159+
adapter.send_message_chain_to_user = _send_user
160+
session = MessageSesion(
161+
platform_name="dingtalk",
162+
message_type=MessageType.FRIEND_MESSAGE,
163+
session_id="user_1",
164+
)
165+
await adapter.send_by_session(session, MessageChain([Plain("hello")]))
166+
assert called["ok"] is True
167+
return
168+
169+
if case == "send_with_sesison_typo":
170+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
171+
called = {{"ok": False}}
172+
173+
async def _send_by_session(session, message_chain):
174+
called["ok"] = True
175+
176+
adapter.send_by_session = _send_by_session
177+
session = MessageSesion(
178+
platform_name="dingtalk",
179+
message_type=MessageType.FRIEND_MESSAGE,
180+
session_id="user_1",
181+
)
182+
await adapter.send_with_sesison(session, MessageChain([Plain("hello")]))
183+
assert called["ok"] is True
184+
return
185+
186+
if case == "terminate":
187+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
188+
ws = DummyWS()
189+
adapter.client_.websocket = ws
190+
adapter._shutdown_event = threading.Event()
191+
await adapter.terminate()
192+
assert ws.closed is True
193+
assert adapter._shutdown_event.is_set() is True
194+
return
195+
196+
raise AssertionError(f"Unknown async case: {{case}}")
197+
198+
if case == "init_basic":
199+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
200+
assert adapter.client_id == "client_id"
201+
assert adapter.client_secret == "client_secret"
202+
203+
elif case == "init_creates_client":
204+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
205+
assert adapter.client is not None
206+
assert adapter.client_ is not None
207+
208+
elif case == "meta":
209+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
210+
meta = adapter.meta()
211+
assert meta.name == "dingtalk"
212+
assert meta.id == "dingtalk_test"
213+
214+
elif case == "id_with_prefix":
215+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
216+
assert adapter._id_to_sid("$:LWCP_v1:$abc") == "abc"
217+
218+
elif case == "id_without_prefix":
219+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
220+
assert adapter._id_to_sid("abc") == "abc"
221+
222+
elif case == "id_none":
223+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
224+
assert adapter._id_to_sid(None) == "unknown"
225+
226+
elif case == "id_empty":
227+
adapter = DingtalkPlatformAdapter(_cfg(), {{}}, asyncio.Queue())
228+
assert adapter._id_to_sid("") == "unknown"
229+
230+
elif case in {{"send_group", "send_private", "send_with_sesison_typo", "terminate"}}:
231+
asyncio.run(_run_async_case())
232+
233+
else:
234+
raise AssertionError(f"Unknown case: {{case}}")
235+
"""
236+
proc = _run_python(code)
237+
assert proc.returncode == 0, (
238+
"DingTalk subprocess test failed.\n"
239+
f"case={case}\n"
240+
f"stdout:\n{proc.stdout}\n"
241+
f"stderr:\n{proc.stderr}\n"
242+
)
243+
244+
245+
class TestDingtalkAdapterInit:
246+
def test_init_basic(self):
247+
_assert_dingtalk_case("init_basic")
248+
249+
def test_init_creates_client(self):
250+
_assert_dingtalk_case("init_creates_client")
251+
252+
253+
class TestDingtalkAdapterMetadata:
254+
def test_meta_returns_correct_metadata(self):
255+
_assert_dingtalk_case("meta")
256+
257+
258+
class TestDingtalkAdapterIdConversion:
259+
def test_id_to_sid_with_prefix(self):
260+
_assert_dingtalk_case("id_with_prefix")
261+
262+
def test_id_to_sid_without_prefix(self):
263+
_assert_dingtalk_case("id_without_prefix")
264+
265+
def test_id_to_sid_with_none(self):
266+
_assert_dingtalk_case("id_none")
267+
268+
def test_id_to_sid_with_empty_string(self):
269+
_assert_dingtalk_case("id_empty")
270+
271+
272+
class TestDingtalkAdapterSendMessage:
273+
def test_send_by_session_group_message(self):
274+
_assert_dingtalk_case("send_group")
275+
276+
def test_send_by_session_private_message(self):
277+
_assert_dingtalk_case("send_private")
278+
279+
280+
class TestDingtalkAdapterTypoCompatibility:
281+
def test_send_with_sesisp_typo(self):
282+
_assert_dingtalk_case("send_with_sesison_typo")
283+
284+
285+
class TestDingtalkAdapterTerminate:
286+
def test_terminate(self):
287+
_assert_dingtalk_case("terminate")

0 commit comments

Comments
 (0)