Files
MoviePilot/tests/test_cli_agent_review_runtime.py
Aqr-K d51320103c fix: make local setup and bundled skill operations recoverable (#7150)
* fix: make local setup and bundled skill operations recoverable

* test: verify user updates with real transaction runners
2026-10-23 06:49:13 +08:00

200 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
命令行 Agent 正式组合根、持久化和资源收口的隔离进程回归。
"""
import os
import subprocess
import sys
from pathlib import Path
import coverage
import pytest
@pytest.mark.parametrize("failure", ["", "model", "configuration", "cancel"])
def test_cli_agent_owns_real_runtime_and_closes_resources(tmp_path, failure):
"""
临时 SQLite 和真实 Agent 入口只替换模型图,成功及失败后均释放运行时 owner。
"""
code = r'''
import asyncio
import json
import os
from types import SimpleNamespace
if os.environ.get("CLI_REVIEW_COVERAGE_FILE"):
import atexit
import coverage
measurement = coverage.Coverage(
data_file=os.environ["CLI_REVIEW_COVERAGE_FILE"],
data_suffix=True,
config_file=os.environ["CLI_REVIEW_COVERAGE_CONFIG"],
source=["app"],
branch=True,
)
measurement.start()
def save_coverage():
"""
子进程退出时保存真实执行轨迹,唯一后缀由父pytest-cov统一合并。
"""
measurement.stop()
measurement.save()
atexit.register(save_coverage)
from app.testing.bootstrap import ensure_sites_stub, isolate_config_dir
isolate_config_dir()
ensure_sites_stub()
import pytest
from app.testing.network import block_real_network
network_patch = pytest.MonkeyPatch()
network_guard = block_real_network.__wrapped__(network_patch)
next(network_guard)
from app.startup.initializers.cache import configure_cache_dependencies
configure_cache_dependencies()
from app.runtime.config import settings
settings.AI_AGENT_ENABLE = True
settings.LLM_PROVIDER = "openai"
settings.LLM_MODEL = "review"
settings.LLM_API_KEY = "review-not-a-secret"
from langchain_core.messages import AIMessage
from app.agent.loader import get_agent_manager, get_running_agent_manager
from app.agent.orchestrator import MoviePilotAgent
from app.application.configuration import get_api_runtime_config_snapshot
from app.application.chain.context import get_chain_runtime_context
from app.application.messaging.chat import get_configured_agent_chat_persistence
from app.db.oper.agentchat import AgentChatOper
from app.db import session as database_session
from app.db.session import SessionFactory
from app.runtime.loop import main_loop_registry
from app.startup.composition import configuration
from app.startup.composition.database import database_runtime_active
from scripts.local_setup import run_agent_request
seen = []
closed = []
failure = os.environ["CLI_REVIEW_FAILURE"]
original_close_database = database_session.close_database
async def observe_database_close():
"""
在真实引擎释放前读取持久消息,观察后继续调用实际关闭 owner。
"""
with SessionFactory() as session:
record = AgentChatOper(session).get("cli-review", "review")
closed.append(bool(record and record.agent_messages))
await original_close_database()
database_session.close_database = observe_database_close
async def graph(agent, streaming=False):
"""
模型边界核对真实组合根身份,并返回确定的 LangChain 消息。
"""
del streaming
manager = get_running_agent_manager()
seen.append({
"data": agent._data is not None,
"canonical": manager is get_agent_manager(),
"memory": manager is not None and agent._memory is manager._memory,
"worker": database_runtime_active(),
"configuration": get_api_runtime_config_snapshot().ai_agent_enable,
"persistence": agent._data is not None and agent._data.chat_persistence is get_configured_agent_chat_persistence(),
"loop": main_loop_registry.require() is asyncio.get_running_loop(),
"chain": get_chain_runtime_context().configuration.ai_agent_enable,
})
agent._last_agent_cache_hit = False
state = {}
async def invoke(payload, config):
del config
if failure == "model":
raise RuntimeError("review model failure")
if failure == "cancel":
raise asyncio.CancelledError()
if os.environ.get("CLI_REVIEW_HISTORY"):
assert len(payload["messages"]) >= 3, payload
state["messages"] = [*payload["messages"], AIMessage(content="review answer")]
return state
return SimpleNamespace(ainvoke=invoke, get_state=lambda _config: SimpleNamespace(values=state))
async def no_title(_self, _message):
"""
标题生成同样属于模型边界,本回归只验证主对话。
"""
MoviePilotAgent._create_agent = graph
MoviePilotAgent.prepare_chat_title = no_title
MoviePilotAgent._should_stream = lambda _self: False
MoviePilotAgent._learning_enabled = lambda _self: False
if failure == "configuration":
async def fail_configuration(**_kwargs):
raise RuntimeError("review configuration failure")
configuration.compose_configuration = fail_configuration
try:
result = run_agent_request(message=" review prompt ", session_id="cli-review", new_session=False, user_id="review")
except RuntimeError as error:
assert failure == "configuration" and "review configuration failure" in str(error), str(error)
except asyncio.CancelledError:
assert failure == "cancel"
else:
assert failure != "configuration", "正式配置组合根没有执行"
assert len(seen) == 1 and all(seen[0].values()), seen
if not failure:
assert result == {"session_id":"cli-review", "result":"review answer"}, result
assert closed == [True], "CLI 对话未提交到临时 SQLite"
else:
assert result["result"], result
assert len(closed) == 1, "引擎关闭 owner 没有执行"
assert not database_runtime_active(), "数据库 worker 未关闭"
assert get_running_agent_manager() is None, "Agent 能力仍处于运行态"
assert main_loop_registry.current is None, "CLI 主循环登记未释放"
if seen:
from app.agent.manager import agent_manager
assert not agent_manager._accepting_tasks and not agent_manager.active_agents
assert agent_manager._idle_cleanup_task is None
assert agent_manager._memory.cleanup_task is None
try:
next(network_guard)
except StopIteration:
pass
network_patch.undo()
print("CLI_REVIEW_OK")
'''
environment = {**os.environ, "CONFIG_DIR": str(tmp_path / "config"), "CLI_REVIEW_FAILURE": failure}
environment.pop("DEBUG", None)
active_coverage = coverage.Coverage.current()
if active_coverage is not None:
environment["CLI_REVIEW_COVERAGE_FILE"] = active_coverage.get_data().data_filename()
environment["CLI_REVIEW_COVERAGE_CONFIG"] = str(Path(__file__).parents[1] / ".coveragerc")
completed = subprocess.run(
[sys.executable, "-c", code],
cwd=Path(__file__).parents[1],
env=environment,
capture_output=True,
text=True,
timeout=60,
check=False,
)
assert completed.returncode == 0, completed.stdout + completed.stderr
assert "CLI_REVIEW_OK" in completed.stdout
if not failure:
replay = subprocess.run(
[sys.executable, "-c", code],
cwd=Path(__file__).parents[1],
env={**environment, "CLI_REVIEW_HISTORY": "1"},
capture_output=True,
text=True,
timeout=60,
check=False,
)
assert replay.returncode == 0, replay.stdout + replay.stderr
assert "CLI_REVIEW_OK" in replay.stdout