fix(channels): ignore confirmations after connect cancellation
This commit is contained in:
@@ -127,9 +127,16 @@ class FeishuConnectStore:
|
|||||||
session.last_error = str(exc)
|
session.last_error = str(exc)
|
||||||
return _pending_payload(session)
|
return _pending_payload(session)
|
||||||
|
|
||||||
session.domain = str(result.get("domain") or session.domain)
|
|
||||||
status = result.get("status")
|
status = result.get("status")
|
||||||
if status == "succeeded":
|
if status == "succeeded":
|
||||||
|
if self._sessions.get(session_id) is not session:
|
||||||
|
return {
|
||||||
|
"session_id": session_id,
|
||||||
|
"instance_id": session.instance_id,
|
||||||
|
"status": "cancelled",
|
||||||
|
"message": "Feishu connection cancelled.",
|
||||||
|
}
|
||||||
|
session.domain = str(result.get("domain") or session.domain)
|
||||||
session.instance_id = feishu.save_registration_result(
|
session.instance_id = feishu.save_registration_result(
|
||||||
result,
|
result,
|
||||||
instance_id=session.instance_id,
|
instance_id=session.instance_id,
|
||||||
@@ -145,6 +152,7 @@ class FeishuConnectStore:
|
|||||||
"app_id": result.get("app_id"),
|
"app_id": result.get("app_id"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
session.domain = str(result.get("domain") or session.domain)
|
||||||
if status == "failed":
|
if status == "failed":
|
||||||
self._sessions.pop(session_id, None)
|
self._sessions.pop(session_id, None)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import threading
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from nanobot.channels.feishu import runtime as feishu
|
||||||
|
from nanobot.channels.feishu.connect import FeishuConnectStore
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_feishu_cancel_wins_over_inflight_confirmation(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
poll_started = threading.Event()
|
||||||
|
release_poll = threading.Event()
|
||||||
|
saved_results: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
monkeypatch.setattr(feishu, "_init_registration", lambda _domain: None)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
feishu,
|
||||||
|
"_begin_registration",
|
||||||
|
lambda _domain: {
|
||||||
|
"device_code": "device-cancel",
|
||||||
|
"qr_url": "https://qr.example/cancel",
|
||||||
|
"expire_in": 600,
|
||||||
|
"interval": 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def fake_poll_registration_once(**_kwargs: Any) -> dict[str, str]:
|
||||||
|
poll_started.set()
|
||||||
|
assert release_poll.wait(timeout=5)
|
||||||
|
return {
|
||||||
|
"status": "succeeded",
|
||||||
|
"domain": "feishu",
|
||||||
|
"app_id": "late-app",
|
||||||
|
"app_secret": "late-secret",
|
||||||
|
}
|
||||||
|
|
||||||
|
def fake_save_registration_result(
|
||||||
|
result: dict[str, Any],
|
||||||
|
**_kwargs: Any,
|
||||||
|
) -> str:
|
||||||
|
saved_results.append(result)
|
||||||
|
return "default"
|
||||||
|
|
||||||
|
monkeypatch.setattr(feishu, "poll_registration_once", fake_poll_registration_once)
|
||||||
|
monkeypatch.setattr(feishu, "save_registration_result", fake_save_registration_result)
|
||||||
|
|
||||||
|
store = FeishuConnectStore()
|
||||||
|
started = await store.handle("start", {})
|
||||||
|
query = {"session_id": [started["session_id"]]}
|
||||||
|
poll_task = asyncio.create_task(store.handle("poll", query))
|
||||||
|
assert await asyncio.to_thread(poll_started.wait, 5)
|
||||||
|
|
||||||
|
cancelled = await store.handle("cancel", query)
|
||||||
|
release_poll.set()
|
||||||
|
completed = await poll_task
|
||||||
|
|
||||||
|
assert cancelled["status"] == "cancelled"
|
||||||
|
assert completed["status"] == "cancelled"
|
||||||
|
assert saved_results == []
|
||||||
@@ -130,6 +130,12 @@ class WeixinConnectStore:
|
|||||||
|
|
||||||
status = status_data.get("status", "")
|
status = status_data.get("status", "")
|
||||||
if status == "confirmed":
|
if status == "confirmed":
|
||||||
|
if self._sessions.get(session_id) is not session:
|
||||||
|
return {
|
||||||
|
"session_id": session_id,
|
||||||
|
"status": "cancelled",
|
||||||
|
"message": "WeChat login cancelled.",
|
||||||
|
}
|
||||||
token = str(status_data.get("bot_token", "") or "")
|
token = str(status_data.get("bot_token", "") or "")
|
||||||
if not token:
|
if not token:
|
||||||
self._sessions.pop(session_id, None)
|
self._sessions.pop(session_id, None)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@@ -97,3 +98,52 @@ async def test_weixin_reconnect_keeps_existing_account_until_scan_succeeds(
|
|||||||
cancelled = await store.cancel(started["session_id"])
|
cancelled = await store.cancel(started["session_id"])
|
||||||
assert cancelled["status"] == "cancelled"
|
assert cancelled["status"] == "cancelled"
|
||||||
assert json.loads(state_file.read_text(encoding="utf-8")) == existing
|
assert json.loads(state_file.read_text(encoding="utf-8")) == existing
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_weixin_cancel_wins_over_inflight_confirmation(
|
||||||
|
tmp_path,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
state_dir = tmp_path / "weixin-state"
|
||||||
|
config_path = tmp_path / "config.json"
|
||||||
|
save_config(
|
||||||
|
Config.model_validate({"channels": {"weixin": {"stateDir": str(state_dir)}}}),
|
||||||
|
config_path,
|
||||||
|
)
|
||||||
|
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
|
||||||
|
|
||||||
|
poll_started = asyncio.Event()
|
||||||
|
release_poll = asyncio.Event()
|
||||||
|
|
||||||
|
async def fake_fetch_qr_code(self: WeixinChannel) -> tuple[str, str]:
|
||||||
|
return "qr-cancel", "https://qr.example/cancel"
|
||||||
|
|
||||||
|
async def fake_api_get_with_base(
|
||||||
|
self: WeixinChannel,
|
||||||
|
**_kwargs: Any,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
poll_started.set()
|
||||||
|
await release_poll.wait()
|
||||||
|
return {
|
||||||
|
"status": "confirmed",
|
||||||
|
"bot_token": "late-token",
|
||||||
|
"ilink_user_id": "late-user",
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(WeixinChannel, "_fetch_qr_code", fake_fetch_qr_code)
|
||||||
|
monkeypatch.setattr(WeixinChannel, "_api_get_with_base", fake_api_get_with_base)
|
||||||
|
|
||||||
|
store = WeixinConnectStore()
|
||||||
|
started = await store.handle("start", {})
|
||||||
|
query = {"session_id": [started["session_id"]]}
|
||||||
|
poll_task = asyncio.create_task(store.handle("poll", query))
|
||||||
|
await asyncio.wait_for(poll_started.wait(), timeout=5)
|
||||||
|
|
||||||
|
cancelled = await store.handle("cancel", query)
|
||||||
|
release_poll.set()
|
||||||
|
completed = await poll_task
|
||||||
|
|
||||||
|
assert cancelled["status"] == "cancelled"
|
||||||
|
assert completed["status"] == "cancelled"
|
||||||
|
assert not (state_dir / "account.json").exists()
|
||||||
|
|||||||
Reference in New Issue
Block a user