"""LLM 后台任务生命周期直测(架构保养第二轮候选 1)。 分析 / 对比 / judge 复核三条 LLM 任务链收进各自的 TaskRegistry: 强引用防 GC、按 id 幂等、shutdown 统一收敛;启动清扫把滞留的 generating 行标记为 failed。 """ import asyncio import pytest from agenteval.evaluation import analysis, comparison from agenteval.exploration import judge from agenteval.storage.repository import ( CampaignAnalysisRepository, CampaignPeriodComparisonRepository, ) from sqlmodel import Session, SQLModel, create_engine @pytest.fixture() def db_session(tmp_path): from agenteval.storage.db import ( # noqa: F401 CampaignAnalysisDB, CampaignDB, CampaignPeriodComparisonDB, ) engine = create_engine( f"sqlite:///{tmp_path / 'llmtasks.db'}", connect_args={"check_same_thread": False}, ) SQLModel.metadata.create_all(engine) session = Session(engine) try: yield session finally: session.close() engine.dispose() async def test_start_analysis_registers_in_registry(monkeypatch): monkeypatch.setattr(analysis, "execute_campaign_analysis", lambda *a, **kw: asyncio.sleep(0)) analysis.start_campaign_analysis("c-1", triggered_by="manual") assert analysis.analysis_registry.is_running("c-1") await analysis.analysis_registry.shutdown_all() assert not analysis.analysis_registry.is_running("c-1") async def test_start_comparison_registers_in_registry(monkeypatch): monkeypatch.setattr(comparison, "execute_campaign_comparison", lambda *a, **kw: asyncio.sleep(0)) comparison.start_campaign_comparison("c-2", triggered_by="manual") assert comparison.comparison_registry.is_running("c-2") await comparison.comparison_registry.shutdown_all() async def test_start_judge_registers_in_registry(monkeypatch): monkeypatch.setattr(judge, "execute_judge_review", lambda *a, **kw: asyncio.sleep(0)) judge.start_judge_review("s-1") assert judge.judge_registry.is_running("s-1") await judge.judge_registry.shutdown_all() async def test_launch_is_idempotent_for_live_id(monkeypatch): gate = asyncio.Event() async def hang(*args, **kwargs): await gate.wait() monkeypatch.setattr(analysis, "execute_campaign_analysis", hang) analysis.start_campaign_analysis("c-dup", triggered_by="manual") analysis.start_campaign_analysis("c-dup", triggered_by="manual") assert len(analysis.analysis_registry._tasks) == 1 gate.set() await analysis.analysis_registry.shutdown_all() async def test_shutdown_all_cancels_hanging_task(monkeypatch): async def hang(*args, **kwargs): await asyncio.Event().wait() monkeypatch.setattr(comparison, "execute_campaign_comparison", hang) comparison.start_campaign_comparison("c-hang", triggered_by="manual") assert comparison.comparison_registry.is_running("c-hang") await comparison.comparison_registry.shutdown_all() assert not comparison.comparison_registry.is_running("c-hang") def test_mark_orphans_failed_flips_generating_analysis(db_session): repo = CampaignAnalysisRepository(db_session) repo.upsert("c-gen", status="generating", triggered_by="auto") repo.upsert("c-done", status="completed", result={"ok": True}, triggered_by="auto") count = repo.mark_orphans_failed() assert count == 1 assert repo.get_by_campaign("c-gen").status == "failed" assert repo.get_by_campaign("c-gen").error assert repo.get_by_campaign("c-done").status == "completed" def test_enqueue_analysis_persists_before_launch(db_session, monkeypatch): from agenteval.evaluation import analysis launched = [] monkeypatch.setattr(analysis, "get_session", lambda: db_session) monkeypatch.setattr(analysis, "start_campaign_analysis", lambda cid, *, triggered_by: launched.append((cid, triggered_by))) analysis.enqueue_campaign_analysis("c-queued", triggered_by="auto") assert launched == [("c-queued", "auto")] row = CampaignAnalysisRepository(db_session).get_by_campaign("c-queued") assert row.status == "queued" assert row.triggered_by == "auto" def test_resume_queued_analysis_relaunches_persisted_jobs(db_session, monkeypatch): from agenteval.evaluation import analysis repo = CampaignAnalysisRepository(db_session) repo.enqueue("c-queued", triggered_by="auto") launched = [] monkeypatch.setattr(analysis, "start_campaign_analysis", lambda cid, *, triggered_by: launched.append((cid, triggered_by))) assert analysis.resume_queued_campaign_analysis(db_session) == 1 assert launched == [("c-queued", "auto")] def test_mark_orphans_failed_flips_generating_comparison(db_session): repo = CampaignPeriodComparisonRepository(db_session) repo.upsert("c-gen", status="generating", baseline_campaign_id="b-1", triggered_by="auto") repo.upsert("c-done", status="completed", baseline_campaign_id="b-2", result={"ok": True}, triggered_by="auto") count = repo.mark_orphans_failed() assert count == 1 assert repo.get_by_campaign("c-gen").status == "failed" assert repo.get_by_campaign("c-gen").error assert repo.get_by_campaign("c-done").status == "completed"