fix(channels): add IM session reset commands
Closes #371. - Add channel-agnostic /new, /reset, and /newsession handling in ChannelRuntime so IM users can start a fresh session without leaving the chat. - Preserve existing session history while clearing only the channel session mapping. - Document in-chat slash commands across README locales. - Normalize pairing slash-command parsing so the documented case-insensitive command behavior matches runtime behavior. Validation: - PYTHONPATH=agent pytest agent/tests/test_channels_runtime.py -q - python -m compileall -q agent/src/channels/runtime.py agent/src/channels/pairing/store.py agent/tests/test_channels_runtime.py - git diff --check - GitHub Actions CI: test
This commit is contained in:
1 parent
4709f4c33c
commit
6ec6bd1314
8 files changed
+287
-4
No files matched your search
@@ -684,6 +684,17 @@ vibe-trading channels pairing --channel telegram list
|
|||||||
|
|
||||||
The built-in adapters cover `websocket`, `telegram`, `slack`, `discord`, `matrix`, `whatsapp`, `signal`, `qq`, `napcat`, `weixin`, `wecom`, `feishu`, `dingtalk`, `msteams`, `email`, and `mochat`. Use narrow extras such as `pip install "vibe-trading-ai[telegram]"`, or install the full channel set with `pip install "vibe-trading-ai[channels]"`.
|
The built-in adapters cover `websocket`, `telegram`, `slack`, `discord`, `matrix`, `whatsapp`, `signal`, `qq`, `napcat`, `weixin`, `wecom`, `feishu`, `dingtalk`, `msteams`, `email`, and `mochat`. Use narrow extras such as `pip install "vibe-trading-ai[telegram]"`, or install the full channel set with `pip install "vibe-trading-ai[channels]"`.
|
||||||
|
|
||||||
|
**In-chat slash commands** (channel-agnostic, work in all 16 adapters):
|
||||||
|
|
||||||
|
| Command | Description |
|
||||||
|
|---------|-------------|
|
||||||
|
| `/new` | Reset the current session — the next message starts a fresh conversation |
|
||||||
|
| `/reset` | Alias for `/new` |
|
||||||
|
| `/newsession` | Alias for `/new` |
|
||||||
|
| `/pairing list` | Show pending sender-pairing requests |
|
||||||
|
|
||||||
|
Commands are case-insensitive and must be sent as the entire message (e.g. `hello /new` is treated as a regular message, not a reset).
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -674,6 +674,17 @@ vibe-trading channels pairing --channel telegram list
|
|||||||
|
|
||||||
تشمل المحولات المدمجة `websocket` و`telegram` و`slack` و`discord` و`matrix` و`whatsapp` و`signal` و`qq` و`napcat` و`weixin` و`wecom` و`feishu` و`dingtalk` و`msteams` و`email` و`mochat`. يمكنك تثبيت منصة محددة مثل `pip install "vibe-trading-ai[telegram]"` أو تثبيت المجموعة كاملة عبر `pip install "vibe-trading-ai[channels]"`.
|
تشمل المحولات المدمجة `websocket` و`telegram` و`slack` و`discord` و`matrix` و`whatsapp` و`signal` و`qq` و`napcat` و`weixin` و`wecom` و`feishu` و`dingtalk` و`msteams` و`email` و`mochat`. يمكنك تثبيت منصة محددة مثل `pip install "vibe-trading-ai[telegram]"` أو تثبيت المجموعة كاملة عبر `pip install "vibe-trading-ai[channels]"`.
|
||||||
|
|
||||||
|
**أوامر الشرطة داخل المحادثة** (مستقلة عن القناة، تعمل في جميع المحولات الـ 16):
|
||||||
|
|
||||||
|
| الأمر | الوصف |
|
||||||
|
|-------|-------|
|
||||||
|
| `/new` | إعادة تعيين الجلسة الحالية — الرسالة التالية تبدأ محادثة جديدة |
|
||||||
|
| `/reset` | اسم مستعار لـ `/new` |
|
||||||
|
| `/newsession` | اسم مستعار لـ `/new` |
|
||||||
|
| `/pairing list` | عرض طلبات sender pairing المعلقة |
|
||||||
|
|
||||||
|
الأوامر لا تحس بحالة الأحرف ويجب إرسالها كرسالة كاملة (مثلاً `hello /new` تُعامل كرسالة عادية وليس كأمر إعادة تعيين).
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -671,6 +671,17 @@ vibe-trading channels pairing --channel telegram list
|
|||||||
|
|
||||||
Built-in adapters は `websocket`、`telegram`、`slack`、`discord`、`matrix`、`whatsapp`、`signal`、`qq`、`napcat`、`weixin`、`wecom`、`feishu`、`dingtalk`、`msteams`、`email`、`mochat` です。個別に `pip install "vibe-trading-ai[telegram]"` を使うか、全チャンネル分を `pip install "vibe-trading-ai[channels]"` で入れられます。
|
Built-in adapters は `websocket`、`telegram`、`slack`、`discord`、`matrix`、`whatsapp`、`signal`、`qq`、`napcat`、`weixin`、`wecom`、`feishu`、`dingtalk`、`msteams`、`email`、`mochat` です。個別に `pip install "vibe-trading-ai[telegram]"` を使うか、全チャンネル分を `pip install "vibe-trading-ai[channels]"` で入れられます。
|
||||||
|
|
||||||
|
**チャット内スラッシュコマンド**(チャンネル非依存、全 16 adapter で共通):
|
||||||
|
|
||||||
|
| コマンド | 説明 |
|
||||||
|
|---------|------|
|
||||||
|
| `/new` | 現在のセッションをリセット——次のメッセージで新しい会話を開始 |
|
||||||
|
| `/reset` | `/new` のエイリアス |
|
||||||
|
| `/newsession` | `/new` のエイリアス |
|
||||||
|
| `/pairing list` | 保留中の sender pairing リクエストを表示 |
|
||||||
|
|
||||||
|
コマンドは大文字小文字を区別せず、メッセージ全体として送信する必要があります(例:`hello /new` はリセットではなく通常メッセージとして処理されます)。
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -674,6 +674,17 @@ vibe-trading channels pairing --channel telegram list
|
|||||||
|
|
||||||
Built-in adapters는 `websocket`, `telegram`, `slack`, `discord`, `matrix`, `whatsapp`, `signal`, `qq`, `napcat`, `weixin`, `wecom`, `feishu`, `dingtalk`, `msteams`, `email`, `mochat`입니다. 개별 플랫폼은 `pip install "vibe-trading-ai[telegram]"`처럼 설치하거나, 전체 채널 세트는 `pip install "vibe-trading-ai[channels]"`로 설치할 수 있습니다.
|
Built-in adapters는 `websocket`, `telegram`, `slack`, `discord`, `matrix`, `whatsapp`, `signal`, `qq`, `napcat`, `weixin`, `wecom`, `feishu`, `dingtalk`, `msteams`, `email`, `mochat`입니다. 개별 플랫폼은 `pip install "vibe-trading-ai[telegram]"`처럼 설치하거나, 전체 채널 세트는 `pip install "vibe-trading-ai[channels]"`로 설치할 수 있습니다.
|
||||||
|
|
||||||
|
**채팅 내 슬래시 명령어** (채널 무관, 16개 어댑터 모두 공통):
|
||||||
|
|
||||||
|
| 명령어 | 설명 |
|
||||||
|
|--------|------|
|
||||||
|
| `/new` | 현재 세션 초기화 — 다음 메시지에서 새 대화 시작 |
|
||||||
|
| `/reset` | `/new`의 별칭 |
|
||||||
|
| `/newsession` | `/new`의 별칭 |
|
||||||
|
| `/pairing list` | 대기 중인 sender pairing 요청 표시 |
|
||||||
|
|
||||||
|
명령어는 대소문자를 구분하지 않으며, 전체 메시지로 전송해야 합니다 (예: `hello /new`은 초기화가 아닌 일반 메시지로 처리됩니다).
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -668,6 +668,17 @@ vibe-trading channels pairing --channel telegram list
|
|||||||
|
|
||||||
内置适配器包括 `websocket`、`telegram`、`slack`、`discord`、`matrix`、`whatsapp`、`signal`、`qq`、`napcat`、`weixin`、`wecom`、`feishu`、`dingtalk`、`msteams`、`email` 和 `mochat`。可按需安装单个平台,例如 `pip install "vibe-trading-ai[telegram]"`,也可以一次安装全量通道依赖:`pip install "vibe-trading-ai[channels]"`。
|
内置适配器包括 `websocket`、`telegram`、`slack`、`discord`、`matrix`、`whatsapp`、`signal`、`qq`、`napcat`、`weixin`、`wecom`、`feishu`、`dingtalk`、`msteams`、`email` 和 `mochat`。可按需安装单个平台,例如 `pip install "vibe-trading-ai[telegram]"`,也可以一次安装全量通道依赖:`pip install "vibe-trading-ai[channels]"`。
|
||||||
|
|
||||||
|
**聊天内斜杠命令**(通道无关,全部 16 个适配器通用):
|
||||||
|
|
||||||
|
| 命令 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `/new` | 重置当前会话——下一条消息将开启一段新对话 |
|
||||||
|
| `/reset` | `/new` 的别名 |
|
||||||
|
| `/newsession` | `/new` 的别名 |
|
||||||
|
| `/pairing list` | 显示待处理的 sender pairing 请求 |
|
||||||
|
|
||||||
|
命令不区分大小写,且必须作为整条消息发送(例如 `hello /new` 会被当作普通消息而非重置命令)。
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ def handle_pairing_command(channel: str, subcommand_text: str) -> str:
|
|||||||
so it can be used from both the CLI and the agent CommandRouter.
|
so it can be used from both the CLI and the agent CommandRouter.
|
||||||
"""
|
"""
|
||||||
parts = subcommand_text.split()
|
parts = subcommand_text.split()
|
||||||
sub = parts[0] if parts else "list"
|
sub = parts[0].lower() if parts else "list"
|
||||||
arg = parts[1] if len(parts) > 1 else None
|
arg = parts[1] if len(parts) > 1 else None
|
||||||
|
|
||||||
if sub in ("list",):
|
if sub in ("list",):
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class ChannelRuntime:
|
|||||||
async def _handle_inbound(self, msg: InboundMessage) -> None:
|
async def _handle_inbound(self, msg: InboundMessage) -> None:
|
||||||
try:
|
try:
|
||||||
if self._is_pairing_command(msg.content):
|
if self._is_pairing_command(msg.content):
|
||||||
reply = handle_pairing_command(msg.channel, msg.content.split(None, 1)[1] if " " in msg.content else "list")
|
reply = handle_pairing_command(msg.channel, self._pairing_subcommand_text(msg.content))
|
||||||
await self.bus.publish_outbound(
|
await self.bus.publish_outbound(
|
||||||
OutboundMessage(
|
OutboundMessage(
|
||||||
channel=msg.channel,
|
channel=msg.channel,
|
||||||
@@ -119,6 +119,22 @@ class ChannelRuntime:
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self._is_new_session_command(msg.content):
|
||||||
|
old_id = self.reset_session(msg.session_key)
|
||||||
|
if old_id:
|
||||||
|
reply = "✅ Session reset. Your next message will start a new conversation."
|
||||||
|
else:
|
||||||
|
reply = "ℹ️ No active session to reset."
|
||||||
|
await self.bus.publish_outbound(
|
||||||
|
OutboundMessage(
|
||||||
|
channel=msg.channel,
|
||||||
|
chat_id=msg.chat_id,
|
||||||
|
content=reply,
|
||||||
|
metadata={"_channel_runtime": True, "session_reset": True},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
session_id = self._session_for(msg)
|
session_id = self._session_for(msg)
|
||||||
result = await self.session_service.send_message(
|
result = await self.session_service.send_message(
|
||||||
session_id,
|
session_id,
|
||||||
@@ -202,9 +218,34 @@ class ChannelRuntime:
|
|||||||
tmp.write_text(json.dumps(self._session_map, indent=2, ensure_ascii=False), encoding="utf-8")
|
tmp.write_text(json.dumps(self._session_map, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
tmp.replace(self.session_map_path)
|
tmp.replace(self.session_map_path)
|
||||||
|
|
||||||
|
def reset_session(self, session_key: str) -> str | None:
|
||||||
|
"""Remove a session mapping so the next message creates a fresh session.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_key: The channel:chat_id key to reset.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The removed session_id, or None if no mapping existed.
|
||||||
|
"""
|
||||||
|
removed = self._session_map.pop(session_key, None)
|
||||||
|
if removed is not None:
|
||||||
|
self._save_session_map()
|
||||||
|
return removed
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_pairing_command(content: str) -> bool:
|
def _is_pairing_command(content: str) -> bool:
|
||||||
return content.strip() == "/pairing" or content.strip().startswith("/pairing ")
|
stripped = content.strip().lower()
|
||||||
|
return stripped == "/pairing" or stripped.startswith("/pairing ")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _pairing_subcommand_text(content: str) -> str:
|
||||||
|
parts = content.strip().split(None, 1)
|
||||||
|
return parts[1] if len(parts) > 1 else "list"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_new_session_command(content: str) -> bool:
|
||||||
|
"""Check if the message is a session reset command (/new, /reset, /newsession)."""
|
||||||
|
return content.strip().lower() in ("/new", "/reset", "/newsession")
|
||||||
|
|
||||||
|
|
||||||
def _session_id(session: Session | dict[str, Any] | Any) -> str:
|
def _session_id(session: Session | dict[str, Any] | Any) -> str:
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ def test_channel_runtime_handles_pairing_commands_without_agent(tmp_path: Path,
|
|||||||
channel="telegram",
|
channel="telegram",
|
||||||
sender_id="owner",
|
sender_id="owner",
|
||||||
chat_id="chat-1",
|
chat_id="chat-1",
|
||||||
content="/pairing list",
|
content="/PAIRING LIST",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
outbound = await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
outbound = await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
@@ -302,3 +302,190 @@ def test_channel_runtime_handles_pairing_commands_without_agent(tmp_path: Path,
|
|||||||
assert outbound.metadata["_pairing_command"] is True
|
assert outbound.metadata["_pairing_command"] is True
|
||||||
|
|
||||||
asyncio.run(scenario())
|
asyncio.run(scenario())
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_runtime_new_command_resets_session_and_creates_fresh_one(tmp_path: Path) -> None:
|
||||||
|
async def scenario() -> None:
|
||||||
|
from src.channels.runtime import ChannelRuntime
|
||||||
|
|
||||||
|
bus = MessageBus()
|
||||||
|
service = FakeSessionService()
|
||||||
|
runtime = ChannelRuntime(
|
||||||
|
bus=bus,
|
||||||
|
session_service=service,
|
||||||
|
manager=None,
|
||||||
|
session_map_path=tmp_path / "channel_sessions.json",
|
||||||
|
reply_timeout_s=1,
|
||||||
|
poll_interval_s=0.01,
|
||||||
|
)
|
||||||
|
await runtime.start(start_manager=False)
|
||||||
|
try:
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="feishu", sender_id="u1", chat_id="c1", content="hello")
|
||||||
|
)
|
||||||
|
reply1 = await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
assert reply1.metadata["session_id"] == "session-1"
|
||||||
|
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="feishu", sender_id="u1", chat_id="c1", content="/new")
|
||||||
|
)
|
||||||
|
reset_reply = await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
assert "Session reset" in reset_reply.content
|
||||||
|
assert reset_reply.metadata.get("session_reset") is True
|
||||||
|
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="feishu", sender_id="u1", chat_id="c1", content="after reset")
|
||||||
|
)
|
||||||
|
reply2 = await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
assert reply2.metadata["session_id"] == "session-2"
|
||||||
|
finally:
|
||||||
|
await runtime.stop()
|
||||||
|
|
||||||
|
assert service.sent == [("session-1", "hello"), ("session-2", "after reset")]
|
||||||
|
|
||||||
|
asyncio.run(scenario())
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_runtime_new_command_with_no_existing_session(tmp_path: Path) -> None:
|
||||||
|
async def scenario() -> None:
|
||||||
|
from src.channels.runtime import ChannelRuntime
|
||||||
|
|
||||||
|
bus = MessageBus()
|
||||||
|
service = FakeSessionService()
|
||||||
|
runtime = ChannelRuntime(
|
||||||
|
bus=bus,
|
||||||
|
session_service=service,
|
||||||
|
manager=None,
|
||||||
|
session_map_path=tmp_path / "channel_sessions.json",
|
||||||
|
reply_timeout_s=1,
|
||||||
|
poll_interval_s=0.01,
|
||||||
|
)
|
||||||
|
await runtime.start(start_manager=False)
|
||||||
|
try:
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="telegram", sender_id="u1", chat_id="c1", content="/new")
|
||||||
|
)
|
||||||
|
reply = await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
finally:
|
||||||
|
await runtime.stop()
|
||||||
|
|
||||||
|
assert "No active session to reset" in reply.content
|
||||||
|
assert reply.metadata.get("session_reset") is True
|
||||||
|
assert service.sent == []
|
||||||
|
assert len(service.created) == 0
|
||||||
|
|
||||||
|
asyncio.run(scenario())
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_runtime_reset_and_newsession_aliases_work(tmp_path: Path) -> None:
|
||||||
|
async def scenario() -> None:
|
||||||
|
from src.channels.runtime import ChannelRuntime
|
||||||
|
|
||||||
|
bus = MessageBus()
|
||||||
|
service = FakeSessionService()
|
||||||
|
runtime = ChannelRuntime(
|
||||||
|
bus=bus,
|
||||||
|
session_service=service,
|
||||||
|
manager=None,
|
||||||
|
session_map_path=tmp_path / "channel_sessions.json",
|
||||||
|
reply_timeout_s=1,
|
||||||
|
poll_interval_s=0.01,
|
||||||
|
)
|
||||||
|
await runtime.start(start_manager=False)
|
||||||
|
try:
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="discord", sender_id="u1", chat_id="c1", content="hi")
|
||||||
|
)
|
||||||
|
await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="discord", sender_id="u1", chat_id="c1", content="/reset")
|
||||||
|
)
|
||||||
|
reply = await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
assert "Session reset" in reply.content
|
||||||
|
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="discord", sender_id="u1", chat_id="c1", content="hi again")
|
||||||
|
)
|
||||||
|
await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="discord", sender_id="u1", chat_id="c1", content="/newsession")
|
||||||
|
)
|
||||||
|
reply2 = await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
assert "Session reset" in reply2.content
|
||||||
|
finally:
|
||||||
|
await runtime.stop()
|
||||||
|
|
||||||
|
asyncio.run(scenario())
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_runtime_regular_messages_not_intercepted_as_new_session(tmp_path: Path) -> None:
|
||||||
|
async def scenario() -> None:
|
||||||
|
from src.channels.runtime import ChannelRuntime
|
||||||
|
|
||||||
|
bus = MessageBus()
|
||||||
|
service = FakeSessionService()
|
||||||
|
runtime = ChannelRuntime(
|
||||||
|
bus=bus,
|
||||||
|
session_service=service,
|
||||||
|
manager=None,
|
||||||
|
session_map_path=tmp_path / "channel_sessions.json",
|
||||||
|
reply_timeout_s=1,
|
||||||
|
poll_interval_s=0.01,
|
||||||
|
)
|
||||||
|
await runtime.start(start_manager=False)
|
||||||
|
try:
|
||||||
|
for text in ["hello /new world", "/new stuff", "/NEW YORK", "type /new to reset"]:
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="slack", sender_id="u1", chat_id="c1", content=text)
|
||||||
|
)
|
||||||
|
reply = await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
assert reply.metadata.get("session_reset") is not True
|
||||||
|
assert "agent reply:" in reply.content
|
||||||
|
finally:
|
||||||
|
await runtime.stop()
|
||||||
|
|
||||||
|
assert len(service.sent) == 4
|
||||||
|
|
||||||
|
asyncio.run(scenario())
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_runtime_session_map_persisted_after_reset(tmp_path: Path) -> None:
|
||||||
|
import json
|
||||||
|
|
||||||
|
async def scenario() -> None:
|
||||||
|
from src.channels.runtime import ChannelRuntime
|
||||||
|
|
||||||
|
map_path = tmp_path / "channel_sessions.json"
|
||||||
|
bus = MessageBus()
|
||||||
|
service = FakeSessionService()
|
||||||
|
runtime = ChannelRuntime(
|
||||||
|
bus=bus,
|
||||||
|
session_service=service,
|
||||||
|
manager=None,
|
||||||
|
session_map_path=map_path,
|
||||||
|
reply_timeout_s=1,
|
||||||
|
poll_interval_s=0.01,
|
||||||
|
)
|
||||||
|
await runtime.start(start_manager=False)
|
||||||
|
try:
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="feishu", sender_id="u1", chat_id="c1", content="hi")
|
||||||
|
)
|
||||||
|
await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
|
||||||
|
data = json.loads(map_path.read_text(encoding="utf-8"))
|
||||||
|
assert data["feishu:c1"] == "session-1"
|
||||||
|
|
||||||
|
await bus.publish_inbound(
|
||||||
|
InboundMessage(channel="feishu", sender_id="u1", chat_id="c1", content="/new")
|
||||||
|
)
|
||||||
|
await asyncio.wait_for(bus.consume_outbound(), timeout=1)
|
||||||
|
|
||||||
|
data = json.loads(map_path.read_text(encoding="utf-8"))
|
||||||
|
assert "feishu:c1" not in data
|
||||||
|
finally:
|
||||||
|
await runtime.stop()
|
||||||
|
|
||||||
|
asyncio.run(scenario())
|
||||||
Reference in new issue
Block a user