44import asyncio
55import pytest
66
7+ from src .chat .heart_flow .heartFC_utils import CycleDetail
8+ from src .common .utils .utils_config import ChatConfigUtils
9+ from src .config .config import global_config
710from src .core .tooling import ToolAvailabilityContext , ToolExecutionResult , ToolInvocation
811from src .llm_models .payload_content .tool_option import ToolCall
12+ from src .maisaka import reasoning_engine as reasoning_engine_module
913from src .maisaka .builtin_tool import get_timing_tools
1014from src .maisaka .chat_loop_service import ChatResponse , MaisakaChatLoopService
1115from src .maisaka .context_messages import AssistantMessage , TIMING_GATE_INVALID_TOOL_HINT_SOURCE
16+ from src .maisaka .history_post_processor import HistoryPostProcessResult
1217from src .maisaka .reasoning_engine import MaisakaReasoningEngine
1318from src .maisaka .runtime import MaisakaHeartFlowChatting
1419
@@ -238,6 +243,7 @@ def test_forced_timing_trigger_bypasses_message_frequency_threshold() -> None:
238243 _internal_turn_queue = asyncio .Queue (),
239244 _has_pending_messages = lambda : True ,
240245 _get_pending_message_count = lambda : 1 ,
246+ _is_reply_frequency_silent = lambda : False ,
241247 _has_forced_timing_trigger = lambda : True ,
242248 _cancel_deferred_message_turn_task = lambda : None ,
243249 )
@@ -253,6 +259,110 @@ def _fail_get_message_trigger_threshold() -> int:
253259 assert runtime ._internal_turn_queue .get_nowait () == "message"
254260
255261
262+ def test_zero_reply_frequency_keeps_effective_zero (monkeypatch : pytest .MonkeyPatch ) -> None :
263+ runtime = object .__new__ (MaisakaHeartFlowChatting )
264+ runtime .session_id = "test-session"
265+ runtime .chat_stream = SimpleNamespace (is_group_session = True )
266+ runtime ._talk_frequency_adjust = 1.0
267+
268+ monkeypatch .setattr (global_config .chat , "talk_value" , 0.0 )
269+ monkeypatch .setattr (
270+ ChatConfigUtils ,
271+ "get_talk_value" ,
272+ staticmethod (lambda session_id , is_group_chat = None : 1.0 ),
273+ )
274+
275+ assert runtime ._get_effective_reply_frequency () == 0.0
276+ assert runtime ._is_reply_frequency_silent () is True
277+
278+
279+ def test_zero_reply_frequency_schedules_silent_turn_before_forced_trigger () -> None :
280+ runtime = SimpleNamespace (
281+ _STATE_WAIT = "wait" ,
282+ _agent_state = "stop" ,
283+ _message_turn_scheduled = False ,
284+ _internal_turn_queue = asyncio .Queue (),
285+ _has_pending_messages = lambda : True ,
286+ _get_pending_message_count = lambda : 1 ,
287+ _is_reply_frequency_silent = lambda : True ,
288+ _cancel_deferred_message_turn_task = lambda : None ,
289+ )
290+
291+ def _fail_has_forced_timing_trigger () -> bool :
292+ raise AssertionError ("回复频率为 0 时不应进入 @/提及强制触发分支" )
293+
294+ runtime ._has_forced_timing_trigger = _fail_has_forced_timing_trigger
295+
296+ MaisakaHeartFlowChatting ._schedule_message_turn (runtime ) # type: ignore[arg-type]
297+
298+ assert runtime ._message_turn_scheduled is True
299+ assert runtime ._internal_turn_queue .get_nowait () == "message"
300+
301+
302+ @pytest .mark .asyncio
303+ async def test_silent_post_process_skips_mid_term_summary_but_keeps_learning (
304+ monkeypatch : pytest .MonkeyPatch ,
305+ ) -> None :
306+ removed_messages = [SimpleNamespace (count_in_context = True )]
307+ final_history = [SimpleNamespace (count_in_context = True )]
308+ process_result = HistoryPostProcessResult (
309+ history = final_history ,
310+ removed_messages = removed_messages ,
311+ removed_count = 1 ,
312+ changed_count = 1 ,
313+ remaining_context_count = 1 ,
314+ )
315+ trim_logs : list [tuple [int , int ]] = []
316+ learning_messages : list [object ] = []
317+
318+ async def _fake_trigger_learning (messages : object ) -> None :
319+ learning_messages .append (messages )
320+
321+ runtime = SimpleNamespace (
322+ _chat_history = [SimpleNamespace (count_in_context = True )],
323+ _max_context_size = 1 ,
324+ log_prefix = "[test]" ,
325+ session_id = "test-session" ,
326+ _log_history_trimmed = lambda removed_count , remaining_count : trim_logs .append (
327+ (removed_count , remaining_count )
328+ ),
329+ _trigger_trimmed_history_learning = _fake_trigger_learning ,
330+ )
331+ engine = MaisakaReasoningEngine (runtime ) # type: ignore[arg-type]
332+ scheduled_coroutines : list [object ] = []
333+
334+ def _fake_create_task (coro : object ) -> SimpleNamespace :
335+ scheduled_coroutines .append (coro )
336+ return SimpleNamespace ()
337+
338+ async def _fail_build_mid_term_memory_message (* args : object , ** kwargs : object ) -> None :
339+ del args , kwargs
340+ raise AssertionError ("静默模式裁切历史不应生成中期记忆摘要" )
341+
342+ monkeypatch .setattr (
343+ reasoning_engine_module ,
344+ "process_chat_history_after_cycle" ,
345+ lambda * args , ** kwargs : process_result ,
346+ )
347+ monkeypatch .setattr (
348+ reasoning_engine_module ,
349+ "build_mid_term_memory_message" ,
350+ _fail_build_mid_term_memory_message ,
351+ )
352+ monkeypatch .setattr (reasoning_engine_module .asyncio , "create_task" , _fake_create_task )
353+
354+ await engine ._post_process_chat_history_after_cycle (
355+ CycleDetail (cycle_id = 1 ),
356+ enable_mid_term_memory = False ,
357+ )
358+ for coro in scheduled_coroutines :
359+ await coro
360+
361+ assert runtime ._chat_history == final_history
362+ assert trim_logs == [(1 , 1 )]
363+ assert learning_messages == [removed_messages ]
364+
365+
256366def test_finish_tool_is_not_written_back_to_history () -> None :
257367 finish_call = ToolCall (call_id = "finish-call" , func_name = "finish" , args = {})
258368 reply_call = ToolCall (call_id = "reply-call" , func_name = "reply" , args = {})
0 commit comments