Files
daily_stock_analysis/tests/test_feishu_stream_ordering.py
mumu 7f201f3b27 fix: restore #648/#649 agent capabilities on main (#822)
* feat: bot commands & async dispatch — /ask, /chat, /history, /strategies, NL routing (#648)

* feat: bot commands & async dispatch — /ask, /chat, /history, /strategies, NL routing

- CommandDispatcher with dispatch_async() and NL routing
- /ask multi-stock analysis (parallel execution, portfolio overlay)
- /chat free-form conversation with session persistence
- /history per-user session isolation (colon delimiter format)
- /strategies with category grouping and activation status
- Two-layer NL routing: regex pre-filter → LLM intent parsing
- Async webhook handler for FastAPI integration
- Feishu stream: capped ThreadPoolExecutor replaces unbounded threads
- DingTalk stream: direct async handler awaiting

* fix: address PR #648 review comments

- ask.py: simplify validate_args error message, add _merge_code_args() for comma+space separated codes
- chat.py: simplify validate_args error message
- feishu_stream.py: shutdown ThreadPoolExecutor in stop()
- test_ask_command.py, test_bot_dispatcher_async.py: replace sys.modules litellm hack with litellm_stub

* fix(bot): preserve ask strategy args and serialize Feishu replies

* fix(strategies): remove activate() side-effect from /strategies command

/strategies is a read-only listing command but was calling
sm.activate() which mutates the skill manager state.  Replace with
a config-based set lookup (configured_active) to derive /
status without any write operations.  This makes the command
purely read-only and eliminates the risk of altering subsequent
/ask or /chat agent skill sets.

(cherry picked from commit a472b824a8)

* feat: deep research agent & event monitor — /research command, alert rules, scheduler integration (#649)

* feat: deep research agent & event monitor — /research command, alert rules, scheduler integration

- ResearchAgent with 3-phase approach (decompose → research → synthesise)
- Token budget tracking for deep research queries
- /research bot command with aliases (/深研, /deepsearch)
- REST endpoint POST /api/v1/agent/research
- EventMonitor with PriceAlert/VolumeAlert rules
- Scheduler background task system for periodic alert polling
- EventMonitor config validation in system_config_service
- main.py: EventMonitor wiring into schedule mode
- Unsupported rule type rejection at config and runtime level

* fix: address PR #649 review comments

- events.py: fix to_dict_list docstring, validate created_at as float, raise ValueError on non-dict rules
- research.py: remove unused import uuid
- agent.py: replace nested ThreadPoolExecutor with asyncio.wait_for, use Field(default_factory=list), pass stock_code context
- scheduler.py: clamp minimum interval_seconds to 30s
- test_multi_agent.py: reduce sleep 3s→0.05s and timeout 1s→0.01s to speed up tests

* fix: normalize price alert direction and tighten research ticker regex

- events.py from_dict_list: .lower() on deserialized direction so
  _check_price matches correctly (fixes silent missed alerts for
  mixed-case config like {"direction":"Above"})
- research.py: restrict US ticker auto-detection to 1-4 chars
  (was 1-5); avoids misclassifying common words (MACRO, TREND)
  as stock codes in /research topic queries

(cherry picked from commit 28126db5bd)

* fix: reconcile recovered agent features with current main

* fix: address review feedback and backend gate failures

* fix: address remaining review feedback

* fix: restore bot agent gating and research timeout handling
2026-03-27 21:29:50 +08:00

99 lines
2.9 KiB
Python

# -*- coding: utf-8 -*-
"""Unit tests for Feishu Stream message ordering guarantees."""
import threading
import time
import unittest
from datetime import datetime
from bot.models import BotMessage, BotResponse, ChatType
from bot.platforms.feishu_stream import FeishuStreamHandler
class _DummyReplyClient:
def __init__(self):
self.calls = []
self._lock = threading.Lock()
def reply_text(self, message_id, text, at_user=False, user_id=None):
with self._lock:
self.calls.append(
{
"message_id": message_id,
"text": text,
"at_user": at_user,
"user_id": user_id,
}
)
return True
def _make_message(
message_id: str,
*,
user_id: str = "u1",
chat_id: str = "c1",
chat_type: ChatType = ChatType.PRIVATE,
) -> BotMessage:
return BotMessage(
platform="feishu",
message_id=message_id,
user_id=user_id,
user_name=user_id,
chat_id=chat_id,
chat_type=chat_type,
content="/chat hello",
raw_content="/chat hello",
mentioned=True,
timestamp=datetime.now(),
)
class FeishuStreamOrderingTestCase(unittest.TestCase):
def test_same_conversation_is_processed_fifo(self):
reply_client = _DummyReplyClient()
processed = []
def on_message(message: BotMessage) -> BotResponse:
if message.message_id == "m1":
time.sleep(0.05)
processed.append(message.message_id)
return BotResponse.text_response(message.message_id)
handler = FeishuStreamHandler(on_message, reply_client)
try:
handler._enqueue_message(_make_message("m1"))
handler._enqueue_message(_make_message("m2"))
deadline = time.time() + 1.0
while len(reply_client.calls) < 2 and time.time() < deadline:
time.sleep(0.01)
self.assertEqual(processed, ["m1", "m2"])
self.assertEqual(
[call["message_id"] for call in reply_client.calls],
["m1", "m2"],
)
finally:
handler.shutdown(wait=True)
def test_group_chat_uses_user_scoped_ordering_key(self):
handler = FeishuStreamHandler(lambda _message: BotResponse.text_response("ok"), _DummyReplyClient())
try:
key_a = handler._conversation_key(
_make_message("m1", user_id="u1", chat_id="group-1", chat_type=ChatType.GROUP)
)
key_b = handler._conversation_key(
_make_message("m2", user_id="u2", chat_id="group-1", chat_type=ChatType.GROUP)
)
self.assertEqual(key_a, "group-1:u1")
self.assertEqual(key_b, "group-1:u2")
self.assertNotEqual(key_a, key_b)
finally:
handler.shutdown(wait=True)
if __name__ == "__main__":
unittest.main()