mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
* fix(wechat): retain cursor persistence across cancellation * fix(wechat): drain expired auth-state persistence * fix(wechat): drain the complete token expiry transition --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
144 lines
4.9 KiB
Python
144 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from app.channels.message_bus import MessageBus
|
|
from app.channels.wechat import WechatChannel
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_poll_cursor_persistence_drains_across_cancellation(monkeypatch, tmp_path: Path) -> None:
|
|
channel = WechatChannel(
|
|
MessageBus(),
|
|
config={"bot_token": "test-token", "state_dir": str(tmp_path)},
|
|
)
|
|
channel._running = True
|
|
|
|
started = threading.Event()
|
|
release = threading.Event()
|
|
original_save_state = channel._save_state
|
|
requests = 0
|
|
|
|
async def ensure_authenticated() -> bool:
|
|
return True
|
|
|
|
async def request_json(*_args, **_kwargs):
|
|
nonlocal requests
|
|
requests += 1
|
|
if requests == 1:
|
|
return {"ret": 0, "msgs": [], "get_updates_buf": "cursor-next"}
|
|
await asyncio.Future()
|
|
|
|
def blocking_save_state() -> None:
|
|
started.set()
|
|
assert release.wait(timeout=5)
|
|
original_save_state()
|
|
|
|
monkeypatch.setattr(channel, "_ensure_authenticated", ensure_authenticated)
|
|
monkeypatch.setattr(channel, "_request_json", request_json)
|
|
monkeypatch.setattr(channel, "_save_state", blocking_save_state)
|
|
|
|
task = asyncio.create_task(channel._poll_loop())
|
|
try:
|
|
assert await asyncio.to_thread(started.wait, 5)
|
|
|
|
task.cancel()
|
|
await asyncio.sleep(0)
|
|
task.cancel()
|
|
await asyncio.sleep(0)
|
|
assert not task.done()
|
|
|
|
release.set()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await task
|
|
finally:
|
|
release.set()
|
|
if not task.done():
|
|
task.cancel()
|
|
await asyncio.gather(task, return_exceptions=True)
|
|
|
|
persisted = json.loads((tmp_path / "wechat-getupdates.json").read_text(encoding="utf-8"))
|
|
assert persisted["get_updates_buf"] == "cursor-next"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("blocked_write", [None, "cursor", "auth"], ids=["no-cancellation", "cancel-during-cursor", "cancel-during-auth"])
|
|
async def test_poll_token_expiry_finishes_both_writes_before_cancellation(monkeypatch, tmp_path: Path, caplog, blocked_write: str | None) -> None:
|
|
channel = WechatChannel(MessageBus(), config={"bot_token": "expired-token", "state_dir": str(tmp_path)})
|
|
channel._get_updates_buf = "cursor-before-expiry"
|
|
await asyncio.to_thread(channel._save_state)
|
|
await asyncio.to_thread(channel._save_auth_state, status="confirmed", bot_token="expired-token")
|
|
channel._running = True
|
|
|
|
started = threading.Event()
|
|
release = threading.Event()
|
|
original_save_state = channel._save_state
|
|
original_save_auth_state = channel._save_auth_state
|
|
auth_lock_observations = []
|
|
|
|
async def ensure_authenticated() -> bool:
|
|
return True
|
|
|
|
async def request_json(*_args, **_kwargs):
|
|
return {"ret": 1, "errcode": -14}
|
|
|
|
def save_state() -> None:
|
|
if blocked_write == "cursor":
|
|
started.set()
|
|
assert release.wait(timeout=5)
|
|
original_save_state()
|
|
|
|
def save_auth_state(**kwargs):
|
|
auth_lock_observations.append(channel._auth_lock.locked())
|
|
if blocked_write == "auth":
|
|
started.set()
|
|
assert release.wait(timeout=5)
|
|
return original_save_auth_state(**kwargs)
|
|
|
|
monkeypatch.setattr(channel, "_ensure_authenticated", ensure_authenticated)
|
|
monkeypatch.setattr(channel, "_request_json", request_json)
|
|
monkeypatch.setattr(channel, "_save_state", save_state)
|
|
monkeypatch.setattr(channel, "_save_auth_state", save_auth_state)
|
|
|
|
task = asyncio.create_task(channel._poll_loop())
|
|
try:
|
|
if blocked_write is None:
|
|
await asyncio.wait_for(task, timeout=5)
|
|
else:
|
|
assert await asyncio.to_thread(started.wait, 5)
|
|
task.cancel()
|
|
await asyncio.sleep(0)
|
|
task.cancel()
|
|
await asyncio.sleep(0)
|
|
assert not task.done()
|
|
|
|
release.set()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await task
|
|
finally:
|
|
release.set()
|
|
if not task.done():
|
|
task.cancel()
|
|
await asyncio.gather(task, return_exceptions=True)
|
|
|
|
cursor = json.loads((tmp_path / "wechat-getupdates.json").read_text(encoding="utf-8"))
|
|
auth = json.loads((tmp_path / "wechat-auth.json").read_text(encoding="utf-8"))
|
|
assert cursor["get_updates_buf"] == ""
|
|
assert auth["status"] == "expired"
|
|
assert "bot_token" not in auth
|
|
assert channel._bot_token == ""
|
|
assert not channel._running
|
|
assert "bot token expired" in caplog.text
|
|
assert auth_lock_observations == [True]
|
|
assert not channel._auth_lock.locked()
|
|
|
|
restored = WechatChannel(MessageBus(), config={"state_dir": str(tmp_path)})
|
|
await asyncio.to_thread(restored._load_state)
|
|
assert restored._bot_token == ""
|
|
assert restored._get_updates_buf == ""
|