mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 20:05:42 +08:00
* 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>
184 lines
6.1 KiB
Python
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()
|