fix(pairing): keep approvals across transient store read failures

_load() treated any OSError like corruption and returned an empty store. When pairing.json was transiently unreadable, an unapproved DM could deny the sender, generate a pairing code from the empty view, and overwrite the store without its approved senders.

Keep the existing JSONDecodeError reset behavior, but propagate OSError so mutations cannot persist unreadable state. Read-only checks fail closed without writing; mutating /pairing subcommands report temporary unavailability; and the DM pairing path skips one reply instead of crashing the handler.

This mirrors the refuse-to-overwrite strategy used by the cron and trigger stores.
This commit is contained in:
KDB
2026-07-30 19:02:37 +08:00
committed by Xubin Ren
parent e633f867e8
commit 52680dbe19
4 changed files with 144 additions and 5 deletions
+43
View File
@@ -83,6 +83,49 @@ async def test_handle_message_dm_sends_pairing_code(monkeypatch) -> None:
assert msg.metadata.get("_pairing_code") == "ABCD-EFGH"
@pytest.mark.asyncio
async def test_dm_during_transient_store_failure_keeps_approvals(
tmp_path, monkeypatch
) -> None:
"""An unapproved DM while pairing.json is unreadable must not wipe approvals.
The pairing store treated a transient OSError like corruption and returned
an empty store; the DM pairing path then persisted that empty view,
erasing every approved sender.
"""
import builtins
from pathlib import Path
from nanobot.pairing import store
path = tmp_path / "pairing.json"
monkeypatch.setattr(store, "_store_path", lambda: path)
code = store.generate_code("dummy", "friend")
store.approve_code(code)
channel = _DummyChannel({"allowFrom": []}, MessageBus())
real_open = builtins.open
def flaky_open(file, mode="r", *args, **kwargs):
try:
same = Path(file) == path
except TypeError:
same = False
if same and "r" in mode and "+" not in mode:
raise PermissionError(13, "temporarily locked", str(path))
return real_open(file, mode, *args, **kwargs)
with monkeypatch.context() as m:
m.setattr(builtins, "open", flaky_open)
await channel._handle_message(
sender_id="stranger", chat_id="chat1", content="hello", is_dm=True
)
assert channel._sent == []
assert store.is_approved("dummy", "friend") is True
@pytest.mark.asyncio
async def test_handle_message_group_ignores_unknown() -> None:
channel = _DummyChannel({"allowFrom": []}, MessageBus())