Files
MoviePilot/tests/test_agent_session_status.py
Aqr-K 7584d4b9fc fix: isolate agent sessions and retain runtime resource ownership (#7149)
* fix: arbitrate subscription identity and restore workflow state

* fix: isolate agent sessions and retain runtime resource ownership

* test: preserve legacy empty-owner subscription transactions

* test: handle reaped terminal descendants during shutdown polling

* fix: align shared atomic configuration method with lifecycle prerequisite

* test: cover subagent cancellation and completion lifecycle boundaries

---------

Co-authored-by: jxxghp <jxxghp@gmail.com>
2026-10-23 08:20:59 +08:00

184 lines
6.1 KiB
Python

"""
验证会话用量汇总、过期清理及运行状态展示。
"""
import asyncio
from datetime import datetime, timedelta
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from langchain.agents.middleware.types import ModelRequest, ModelResponse
from langchain_core.messages import AIMessage
from app.agent.middleware.usage import UsageMiddleware
from app.agent.manager import AgentManager
from app.chain.message import MessageChain
from app.schemas.types import NotificationChannel
@pytest.fixture(autouse=True)
def _isolate_test_state():
"""
隔离测试之间共享的会话与交互状态,并在结束时清理。
"""
MessageChain._user_sessions.clear()
try:
yield
finally:
MessageChain._user_sessions.clear()
def test_usage_middleware_records_usage_metadata() -> None:
"""
用量中间件记录模型、上下文容量与输入输出消耗。
"""
snapshots = []
middleware = UsageMiddleware(on_usage=snapshots.append)
request = ModelRequest(
model=SimpleNamespace(model="gpt-4o-mini", profile={"max_input_tokens": 128000}),
messages=[],
state={},
runtime=None,
)
response = ModelResponse(
result=[
AIMessage(
content="ok",
usage_metadata={
"input_tokens": 1200,
"output_tokens": 300,
"total_tokens": 1500,
},
)
]
)
async def handler(_: ModelRequest):
return response
result = asyncio.run(middleware.awrap_model_call(request, handler))
assert result is response
assert len(snapshots) == 1
assert snapshots[0]["model"] == "gpt-4o-mini"
assert snapshots[0]["context_window_tokens"] == 128000
assert snapshots[0]["input_tokens"] == 1200
assert snapshots[0]["output_tokens"] == 300
assert snapshots[0]["total_tokens"] == 1500
assert round(abs(snapshots[0]["context_usage_ratio"] - 1200 / 128000), 7) == 0
def test_remote_session_status_sends_usage_summary() -> None:
"""
远程会话状态回复包含用量、估算偏差和运行状态。
"""
chain = MessageChain()
chain._user_sessions[chain._session_user_key("10001", NotificationChannel.Telegram, "telegram-test")] = (
"session-1", datetime.now(),
)
status = {
"session_id": "session-1",
"model": "gpt-4o-mini",
"context_window_tokens": 128000,
"last_input_usage_available": True,
"last_input_tokens": 1200,
"last_output_tokens": 300,
"last_total_tokens": 1500,
"last_context_usage_ratio": 1200 / 128000,
"last_request_estimate_available": True,
"last_estimated_input_tokens": 1300,
"last_estimated_message_tokens": 900,
"last_estimated_system_tokens": 250,
"last_estimated_tool_tokens": 150,
"last_estimated_multimodal_tokens": 0,
"last_estimated_input_ratio": 1300 / 128000,
"last_estimated_over_input_limit": False,
"last_actual_input_tokens": 1200,
"last_estimate_error_tokens": -100,
"last_estimate_error_ratio": -100 / 1200,
"total_input_tokens": 4500,
"total_output_tokens": 1500,
"total_tokens": 6000,
"model_call_count": 4,
"last_updated_at": "2026-04-26 12:34:56",
"is_processing": True,
"pending_messages": 2,
}
with (
patch(
"app.chain.message.get_running_agent_manager",
return_value=SimpleNamespace(get_session_status=lambda **_: status),
),
patch.object(chain, "post_message") as post_message,
):
chain.remote_session_status(
channel=NotificationChannel.Telegram,
userid="10001",
source="telegram-test",
)
notification = post_message.call_args.args[0]
assert notification.title == "当前智能体会话状态"
assert "session-1" in notification.text
assert "gpt-4o-mini" in notification.text
assert "1,200 / 128,000 (0.94%)" in notification.text
assert "最终请求估算: 1,300 / 128,000 (1.02%)" in notification.text
assert "消息 900 / 系统 250 / 工具 150 / 其中图片固定成本 0" in notification.text
assert "实际 1,200 / 误差 -100 (-8.33%)" in notification.text
assert "输入 4,500 / 输出 1,500 / 总计 6,000" in notification.text
assert "运行中" in notification.text
def test_remote_session_status_handles_missing_session() -> None:
"""
没有活跃会话时返回明确提示。
"""
chain = MessageChain()
with patch.object(chain, "post_message") as post_message:
chain.remote_session_status(
channel=NotificationChannel.Telegram,
userid="10001",
source="telegram-test",
)
notification = post_message.call_args.args[0]
assert notification.title == "您当前没有活跃的智能体会话"
def test_get_or_create_session_cleans_expired_session() -> None:
"""
用户会话超过复用窗口时应调度清理旧 Agent 会话。
"""
chain = MessageChain()
chain._user_sessions.clear()
chain._user_sessions["10001"] = (
"old-session",
datetime.now() - timedelta(minutes=chain._session_timeout_minutes + 1),
)
with patch.object(chain, "_schedule_agent_session_clear") as clear_session:
session_id = chain._get_or_create_session_id("10001")
assert session_id != "old-session"
assert chain._user_sessions["10001"][0] == session_id
clear_session.assert_called_once_with("old-session", "10001")
def test_agent_manager_collects_idle_sessions() -> None:
"""
Agent 管理器应只回收超过空闲窗口且未忙碌的会话。
"""
manager = AgentManager()
manager._idle_session_ttl = timedelta(seconds=1)
manager._session_last_used["idle-session"] = (
"10001",
datetime.now() - timedelta(seconds=2),
)
manager._session_last_used["fresh-session"] = ("10002", datetime.now())
assert [("idle-session", "10001")] == manager._expired_idle_sessions()