deer-flow/backend/tests/test_channel_undecodable_state.py
yetuge b57488734d
fix(channels): recover from undecodable runtime config and WeChat state (#7273)
`store.py` already treats a state file whose bytes are not UTF-8 as corruption:
`_read_legacy_file` catches `UnicodeDecodeError` and the three call paths start
fresh or skip the import (#7233, AGENTS.md: "Store JSON decode errors follow
corrupt-file recovery"). Four readers in the same package read the same kind of
file with an explicit `encoding="utf-8"` but catch only
`json.JSONDecodeError`/`OSError`; `UnicodeDecodeError` is a `ValueError`, so it
escapes and takes the caller down instead of following that policy:

* `ChannelRuntimeConfigStore._load` — `ChannelService.start()` and the
  channel-connections router construct the store, so one undecodable
  `runtime-config.json` stops every IM channel from starting, including the
  non-fallback path where a peer's copy should be rewritten from this process's
  snapshot.
* `_read_legacy_file` — the one-time `runtime-config.json` import aborts instead
  of keeping the file and reporting it as corrupt.
* `WechatChannel._load_state` / `_load_auth_state` — `start()` loads both through
  `asyncio.to_thread`, so an undecodable cursor or auth file raises out of the
  channel's own start instead of warning and continuing unauthenticated.

Each reader now names `UnicodeDecodeError` next to the existing handlers, so the
outcome matches the sibling store: a warning, the in-memory fallback, and the
file left in place.
2026-10-22 19:26:38 +08:00

79 lines
3.3 KiB
Python

"""Undecodable channel state follows the corrupt-state recovery policy, not a crash.
``app/channels/store.py`` recovers from a state file whose bytes are not UTF-8
(``_read_legacy_file`` catches ``UnicodeDecodeError`` and starts fresh), and its
AGENTS.md states that rule for channel store JSON. The other state readers in the
same package read the same kind of file with an explicit ``encoding="utf-8"`` but
only catch ``json.JSONDecodeError`` and ``OSError``; ``UnicodeDecodeError`` is a
``ValueError``, not an ``OSError``, so it escapes and the channel cannot start.
Each test writes undecodable bytes into the file that reader owns and asserts the
documented outcome: a warning, the in-memory fallback, and the file left in place.
"""
from __future__ import annotations
import json
import logging
from pathlib import Path
import pytest
from app.channels import runtime_config_store
from app.channels.message_bus import MessageBus
from app.channels.wechat import WechatChannel
UNDECODABLE = b'{"enabled": "\xff"}'
def test_runtime_config_store_starts_fresh_on_undecodable_state(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
path = tmp_path / "channels" / "runtime-config.json"
path.parent.mkdir(parents=True)
path.write_bytes(UNDECODABLE)
with caplog.at_level(logging.WARNING, logger="app.channels.runtime_config_store"):
store = runtime_config_store.ChannelRuntimeConfigStore(path)
assert store.get_provider_config("buzz") is None
assert "Corrupt channel runtime config store" in caplog.text
assert path.read_bytes() == UNDECODABLE
def test_runtime_config_store_reports_the_peer_copy_on_undecodable_state(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
"""The reload path under ``_mutation`` must fall back to this process's copy."""
path = tmp_path / "channels" / "runtime-config.json"
store = runtime_config_store.ChannelRuntimeConfigStore(path)
store.set_provider_config("buzz", {"enabled": True})
path.write_bytes(UNDECODABLE)
with caplog.at_level(logging.WARNING, logger="app.channels.runtime_config_store"):
store.set_provider_config("slack", {"enabled": True})
assert "rewriting it from this process's copy" in caplog.text
assert store.get_provider_config("buzz") == {"enabled": True}
@pytest.mark.asyncio
async def test_wechat_starts_on_undecodable_state(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
(tmp_path / "wechat-getupdates.json").write_bytes(UNDECODABLE)
channel = WechatChannel(MessageBus(), config={"bot_token": "test-token", "state_dir": str(tmp_path)})
with caplog.at_level(logging.WARNING, logger="app.channels.wechat"):
channel._load_state()
assert channel._get_updates_buf == ""
assert "failed to read cursor state" in caplog.text
@pytest.mark.asyncio
async def test_wechat_starts_on_undecodable_auth_state(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
(tmp_path / "wechat-auth.json").write_bytes(UNDECODABLE)
channel = WechatChannel(MessageBus(), config={"bot_token": "", "state_dir": str(tmp_path), "qrcode_login_enabled": True})
with caplog.at_level(logging.WARNING, logger="app.channels.wechat"):
channel._load_auth_state()
assert channel._bot_token == ""
assert "failed to read auth state" in caplog.text
assert json.dumps({}) == "{}"