oclaw/tests/test_wecom_longconn_outbound_drain.py
oliver 420abac9f1 refactor: root-package imports (svc/runtime/interfaces) and fix PYTHONPATH
- Rename platform/ to svc/ to avoid shadowing stdlib platform.
- Replace from oclaw.* with from svc/runtime/interfaces; update -m CLI paths.
- tests/conftest: prepend repo root to sys.path (no parent-folder package name).
- CI: paths and offline_eval script under repo root.
- Ops scripts: PYTHONPATH must be repo root for python -m runtime.* (fixes gateway/WhatsApp sidecar startup).
- Fix default oclaw.json path in tabular/file attachment limits; stabilize attachment test config.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-13 14:51:17 +08:00

88 lines
2.8 KiB
Python

from __future__ import annotations
import json
import queue
import sys
from types import SimpleNamespace
from typing import Any
import pytest
def test_wecom_longconn_drains_outbound_on_idle_timeout(monkeypatch: pytest.MonkeyPatch) -> None:
# Import module under test.
import interfaces.channels.wecom.longconn_runner as m
# Build a fake websocket that raises timeout once, then exits by raising KeyboardInterrupt.
class _TimeoutOnceWs:
def __init__(self) -> None:
self.sent: list[str] = []
self._recv_calls = 0
def settimeout(self, _t: float) -> None:
return None
def recv(self) -> str:
self._recv_calls += 1
if self._recv_calls == 1:
# Mimic websocket-client exception type used in module.
raise WebSocketTimeoutException()
raise KeyboardInterrupt()
def send(self, data: str) -> None:
self.sent.append(data)
def ping(self) -> None:
return None
def close(self) -> None:
return None
ws = _TimeoutOnceWs()
class WebSocketTimeoutException(Exception):
pass
fake_ws_mod = SimpleNamespace(
WebSocketTimeoutException=WebSocketTimeoutException,
create_connection=lambda *_a, **_k: ws,
)
monkeypatch.setitem(sys.modules, "websocket", fake_ws_mod)
# Patch WeComClient to avoid real credentials.
class _Sender:
def __init__(self) -> None:
self.store = SimpleNamespace(
get_setting=lambda _k: "",
set_setting=lambda _k, _v: None,
)
def get_bot_credentials(self) -> tuple[str, str]:
return ("bot", "secret")
sender = _Sender()
# Patch worker threads/queues by replacing queue.Queue with one we can prefill for outbound only.
# We do this by monkeypatching the queue constructor used in _run_ws_forever.
real_queue = queue.Queue
ctor_count = {"n": 0}
def _queue_ctor(*args: Any, **kwargs: Any) -> Any:
ctor_count["n"] += 1
q = real_queue(*args, **kwargs)
# Second queue constructed is outbound_q in _run_ws_forever.
if ctor_count["n"] == 2:
q.put({"callback_req_id": "r1", "response_url": "", "text": "hello", "raw_rep": {}})
return q
monkeypatch.setattr(m.queue, "Queue", _queue_ctor)
# Run; it should exit cleanly via KeyboardInterrupt from fake ws.recv.
assert m._run_ws_forever(sender=sender, deliver_outbound=True, use_response_url=False) == 0 # type: ignore[arg-type]
# Assert that a send happened with expected cmd.
assert ws.sent, "expected outbound to be sent during idle timeout drain"
obj = json.loads(ws.sent[-1])
assert obj.get("cmd") == "aibot_respond_msg"
assert (obj.get("body") or {}).get("markdown", {}).get("content") == "hello"