forked from admin/PlantAI-rag-new
- 后端写稿循环不再落空 paper_progress 块(重载后出现空卡),改落开始撰写文本 - 前端 confirmPaper 时用大纲节标题本地 seed 进度卡,2s 轮询 patch 逐节状态 - 当前节按 current_section_id 标 writing(…动画)
125 lines
5.6 KiB
Python
125 lines
5.6 KiB
Python
"""tests/paper/test_paper_bff_agent.py —— BFF 论文大脑状态机测试(假 MCP server + 假 LLM)。
|
|
|
|
跑通 brief→outline→confirm(写节循环)→complete→artifact 全状态机。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from conftest import _run
|
|
from app import db as bff_db
|
|
from app.routers import docgen as docgen_mod
|
|
from app.services.paper import agent
|
|
|
|
|
|
class FakePaperService:
|
|
"""脚本化 paper 服务:按工具名返回预设结果,模拟写节循环推进。"""
|
|
|
|
def __init__(self):
|
|
self.paper_id = "p1"
|
|
self.s1_done = False
|
|
self.s2_done = False
|
|
|
|
async def call_tool(self, name, arguments):
|
|
if name == "propose_paper_outline":
|
|
return {"ok": True, "paper_id": self.paper_id, "title": "水稻SPL基因家族研究进展",
|
|
"abstract_expect": "综述", "sections": [
|
|
{"section_id": "s1", "title": "引言", "key_points": [], "target_words": 1500},
|
|
{"section_id": "s2", "title": "讨论", "key_points": [], "target_words": 1500},
|
|
]}
|
|
if name == "get_paper_progress":
|
|
sections = [
|
|
{"section_id": "s1", "title": "引言", "status": "done" if self.s1_done else "pending",
|
|
"word_count": 0, "citation_count": 0},
|
|
{"section_id": "s2", "title": "讨论", "status": "done" if self.s2_done else "pending",
|
|
"word_count": 0, "citation_count": 0},
|
|
]
|
|
return {"ok": True, "paper_id": self.paper_id, "status": "writing",
|
|
"sections": sections, "current_section_id": None}
|
|
if name == "write_paper_section":
|
|
sid = arguments["section_id"]
|
|
if sid == "s1":
|
|
self.s1_done = True
|
|
else:
|
|
self.s2_done = True
|
|
return {"ok": True, "section_id": sid, "status": "done",
|
|
"markdown": "内容" * 50, "citations_used": [], "new_citations": []}
|
|
if name == "complete_paper":
|
|
return {"ok": True, "full_markdown": "# 全文\n内容",
|
|
"references": [{"n": 1, "text": "X.", "kind": "title", "source_doc_id": "d1"}],
|
|
"renumber_map": {}, "disclaimer": "d"}
|
|
if name == "get_paper":
|
|
return {"ok": True, "paper_id": self.paper_id, "title": "水稻SPL基因家族研究进展",
|
|
"status": "complete", "full_markdown": "# 全文",
|
|
"references": [{"n": 1, "text": "X.", "kind": "title", "source_doc_id": "d1"}],
|
|
"disclaimer": "d"}
|
|
if name == "cancel_paper":
|
|
return {"ok": True}
|
|
if name == "revise_paper":
|
|
return {"ok": True, "affected_section_ids": ["s1"], "plan": "修订引言"}
|
|
if name == "revise_paper_section":
|
|
return {"ok": True, "section_id": arguments["section_id"], "markdown": "改后", "citations_used": []}
|
|
raise AssertionError(f"unexpected tool: {name}")
|
|
|
|
|
|
@pytest.fixture
|
|
def bff_database(tmp_path):
|
|
_run(bff_db.init_db(tmp_path / "bff.db"))
|
|
_run(bff_db.db.execute(
|
|
"INSERT INTO sessions (id, title, status, meta, created_at, updated_at) "
|
|
"VALUES ('s1', 't', 'idle', '{}', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z')"))
|
|
yield bff_db.db
|
|
_run(bff_db.close_db())
|
|
|
|
|
|
def _fake_brief(complete=True):
|
|
async def _complete_json(system, user, **kwargs):
|
|
if complete:
|
|
return {"topic": "水稻SPL基因家族", "paper_type": "综述", "language": "中文",
|
|
"length": "5000字", "format_req": "GB/T 7714", "style": "学术严谨", "requirements": ""}
|
|
return {"topic": "水稻SPL", "paper_type": "", "language": "", "length": ""}
|
|
return _complete_json
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_insert_artifact(monkeypatch):
|
|
async def _fake(db, **kwargs):
|
|
return "art-fake"
|
|
monkeypatch.setattr(docgen_mod, "insert_artifact", _fake)
|
|
return _fake
|
|
|
|
|
|
def test_trigger_complete_brief_proposes_outline(bff_database, monkeypatch, fake_insert_artifact):
|
|
svc = FakePaperService()
|
|
monkeypatch.setattr(agent, "_new_client", lambda: svc)
|
|
monkeypatch.setattr(agent, "llm_complete_json", _fake_brief(complete=True))
|
|
|
|
blocks = _run(agent.handle_turn(bff_database, "s1", "帮我写一篇水稻SPL基因家族综述"))
|
|
assert blocks[-1]["type"] == "paper_outline"
|
|
assert blocks[-1]["paper_id"] == "p1"
|
|
|
|
rows = _run(bff_database.fetchall("SELECT * FROM messages WHERE session_id = 's1'"))
|
|
assert len(rows) == 1
|
|
assert rows[0]["kind"] == "paper"
|
|
|
|
|
|
def test_trigger_incomplete_brief_returns_form(bff_database, monkeypatch):
|
|
monkeypatch.setattr(agent, "_new_client", lambda: FakePaperService())
|
|
monkeypatch.setattr(agent, "llm_complete_json", _fake_brief(complete=False))
|
|
blocks = _run(agent.handle_turn(bff_database, "s1", "帮我写论文"))
|
|
assert blocks[-1]["type"] == "paper_brief"
|
|
assert blocks[-1]["status"] == "pending"
|
|
|
|
|
|
def test_writing_loop_completes_to_artifact(bff_database, monkeypatch, fake_insert_artifact):
|
|
svc = FakePaperService()
|
|
monkeypatch.setattr(agent, "_new_client", lambda: svc)
|
|
_run(agent.run_writing_loop(bff_database, "p1", "s1"))
|
|
|
|
assert svc.s1_done and svc.s2_done
|
|
rows = _run(bff_database.fetchall("SELECT * FROM messages WHERE session_id = 's1' ORDER BY rowid"))
|
|
# 进度卡由前端确认时本地 seed + 轮询驱动,后端只落「开始撰写」文本 + 逐节小结 + artifact
|
|
assert "开始逐节撰写" in rows[0]["blocks"]
|
|
assert "artifact" in rows[-1]["blocks"]
|
|
assert "art-fake" in rows[-1]["blocks"]
|