import asyncio from pathlib import Path from types import SimpleNamespace from agents.exceptions import MaxTurnsExceeded from app.config import Settings, UserSeed from app.curator import Curator, Runner from app.db import Database def test_tool_loop_uses_no_tool_convergence_repair( tmp_path: Path, monkeypatch ): admin = UserSeed( id="admin", label="管理员", role="admin", access_key_hash="unused", ) database = Database(tmp_path / "repair.sqlite3") database.initialize([admin]) fragment = database.create_fragment("admin", "一个需要收敛的念头") settings = Settings( data_dir=tmp_path, database_path=tmp_path / "repair.sqlite3", users=(admin,), session_secret="unused", deepseek_api_key="test-key", deepseek_base_url="https://api.deepseek.com", deepseek_model="deepseek-v4-pro", cookie_secure=False, auth_disabled=True, ) calls = 0 async def fake_run(*args, **kwargs): nonlocal calls calls += 1 if calls == 1: raise MaxTurnsExceeded("Max turns (6) exceeded") assert kwargs["max_turns"] == 2 assert args[0].tools == [] return SimpleNamespace( final_output=( '{"standalone":true,"reasoning_note":"暂时独立保留",' '"direction_signal":"none","assessments":[]}' ) ) monkeypatch.setattr(Runner, "run", fake_run) curator = Curator(database, settings) asyncio.run(curator.analyze(fragment["id"])) assert calls == 2 assert database.get_fragment(fragment["id"], "admin")[ "analysis_status" ] == "done" run = database.list_agent_runs(1)[0] events = database.list_agent_events(run["id"]) assert run["status"] == "success" assert run["attempt_count"] == 2 assert "convergence_repair_started" in [ event["event_type"] for event in events ] def test_curator_recovers_visible_prior_fragment( tmp_path: Path, monkeypatch ): admin = UserSeed( id="admin", label="管理员", role="admin", access_key_hash="unused", ) database = Database(tmp_path / "recovery.sqlite3") database.initialize([admin]) earlier = database.create_fragment( "admin", "有个新工作可能要做小爱架构。" ) database.apply_assessments("admin", earlier["id"], []) current = database.create_fragment( "admin", "确定要做,先熟悉 harness 5.0。" ) settings = Settings( data_dir=tmp_path, database_path=tmp_path / "recovery.sqlite3", users=(admin,), session_secret="unused", deepseek_api_key="test-key", deepseek_base_url="https://api.deepseek.com", deepseek_model="deepseek-v4-pro", cookie_secure=False, auth_disabled=True, ) async def fake_run(*args, **kwargs): return SimpleNamespace( final_output=( "{" '"direction_signal":"explicit",' '"standalone":false,' '"reasoning_note":"明确的新工作,应形成可跟踪脉络",' '"assessments":[{' '"idea_id":null,' '"title":"小爱新架构开发",' '"summary":"熟悉现有方案并推进新架构开发。",' '"maturity":10,' '"confidence":0.9,' '"motion":"启动中",' '"position":"先熟悉 harness 5.0 核心方案。",' '"tension":"任务已明确,但范围与现有方案尚未理解。",' '"trajectory":"从可能插入的工作变成明确承诺。",' '"possible_moves":["阅读核心方案"],' '"relevance":1,' f'"supporting_fragment_ids":["{earlier["id"]}"]' "}]}" ) ) monkeypatch.setattr(Runner, "run", fake_run) curator = Curator(database, settings) asyncio.run(curator.analyze(current["id"])) idea = database.get_idea( "admin", database.list_ideas("admin")[0]["id"] ) assert idea["title"] == "小爱新架构开发" assert [item["id"] for item in idea["fragments"]] == [ earlier["id"], current["id"], ]