feat: overhaul UI styling, improve templates, enhance services and tests

This commit is contained in:
2026-06-06 00:38:56 +08:00
parent f7f1a4c0cb
commit 904eec392e
38 changed files with 1471 additions and 795 deletions
+62 -32
View File
@@ -51,7 +51,9 @@ class TestDbUpdate:
assert summary.motivation_problem == schema.motivation.problem
assert json.loads(summary.full_json)["title_zh"] == schema.title_zh
def test_paper_title_zh_updated(self, db_session, sample_paper, sample_summary_dict):
def test_paper_title_zh_updated(
self, db_session, sample_paper, sample_summary_dict
):
schema = SummarySchema.model_validate(sample_summary_dict)
_update_summary_in_db(db_session, sample_paper, schema, "normal", "raw")
@@ -85,7 +87,9 @@ class TestDbUpdate:
assert "自然语言处理" in tag_names
assert "大语言模型" in tag_names
def test_existing_tags_not_duplicated(self, db_session, sample_paper, sample_summary_dict):
def test_existing_tags_not_duplicated(
self, db_session, sample_paper, sample_summary_dict
):
"""已存在的标签名(同 name)不会被 AI source 重复插入。"""
# sample_paper 已有 NLP (hf)、LLM (hf)
# 让 AI 输出包含 NLP(与 HF 重复)和 "新标签"(新的)
@@ -157,7 +161,10 @@ class TestSummarizeOneFlow:
def _patch_paths(self, tmp_path):
"""将 data 目录重定向到 tmp_path。"""
with (
patch("app.services.summarizer.paper_dir", lambda aid: tmp_path / "papers" / aid),
patch(
"app.services.summarizer.paper_dir",
lambda aid: tmp_path / "papers" / aid,
),
patch("app.services.pdf_downloader.PAPERS_DIR", tmp_path / "papers"),
patch("app.services.pdf_downloader.TMP_DIR", tmp_path / "tmp"),
patch("app.utils.PAPERS_DIR", tmp_path / "papers"),
@@ -172,7 +179,11 @@ class TestSummarizeOneFlow:
"""pending → processing → done 全流程。"""
with (
patch("app.services.summarizer.download_pdf", new_callable=AsyncMock),
patch("app.services.summarizer.call_pi", new_callable=AsyncMock, return_value=mock_pi_output),
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value=mock_pi_output,
),
):
result = await summarize_one(db_session, sample_paper)
@@ -198,9 +209,7 @@ class TestSummarizeOneFlow:
assert fts_row[0] == "测试论文中文标题"
@pytest.mark.asyncio
async def test_pdf_download_failure(
self, db_session, sample_paper, _patch_paths
):
async def test_pdf_download_failure(self, db_session, sample_paper, _patch_paths):
"""PDF 下载失败 → error_type=pdf_download_failedtmp 被清理。"""
with (
patch(
@@ -256,13 +265,16 @@ class TestSummarizeOneFlow:
self, db_session, sample_paper, _patch_paths
):
"""必填字段缺失 → field_missing → retry → permanent_failure。"""
bad_json = json.dumps({
"title_zh": "", # 空的必填字段
"one_line": "valid line",
"tags": ["tag1"],
"motivation": {"problem": "valid problem"},
"method": {"key_idea": "valid idea"},
}, ensure_ascii=False)
bad_json = json.dumps(
{
"title_zh": "", # 空的必填字段
"one_line": "valid line",
"tags": ["tag1"],
"motivation": {"problem": "valid problem"},
"method": {"key_idea": "valid idea"},
},
ensure_ascii=False,
)
bad_output = f"```json\n{bad_json}\n```"
with (
@@ -314,7 +326,11 @@ class TestSummarizeOneFlow:
"""成功后清理 tmp 目录。"""
with (
patch("app.services.summarizer.download_pdf", new_callable=AsyncMock),
patch("app.services.summarizer.call_pi", new_callable=AsyncMock, return_value=mock_pi_output),
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value=mock_pi_output,
),
):
await summarize_one(db_session, sample_paper)
@@ -359,7 +375,10 @@ class TestBatchSummarize:
@pytest.fixture
def _patch_paths(self, tmp_path):
with (
patch("app.services.summarizer.paper_dir", lambda aid: tmp_path / "papers" / aid),
patch(
"app.services.summarizer.paper_dir",
lambda aid: tmp_path / "papers" / aid,
),
patch("app.services.pdf_downloader.PAPERS_DIR", tmp_path / "papers"),
patch("app.services.pdf_downloader.TMP_DIR", tmp_path / "tmp"),
patch("app.utils.PAPERS_DIR", tmp_path / "papers"),
@@ -390,15 +409,18 @@ class TestBatchSummarize:
# 每个 worker 用独立 session(同一个内存引擎)
from sqlalchemy.orm import sessionmaker as _sm
_TestSession = _sm(bind=db_engine, autoflush=False, autocommit=False)
with (
patch("app.services.summarizer.download_pdf", new_callable=AsyncMock),
patch("app.services.summarizer.call_pi", new_callable=AsyncMock, return_value=mock_pi_output),
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value=mock_pi_output,
),
):
result = await summarize_batch(
db_session, _session_factory=_TestSession
)
result = await summarize_batch(db_session, _session_factory=_TestSession)
assert result["status"] == "success"
assert result["done"] == 3
@@ -432,6 +454,7 @@ class TestBatchSummarize:
db_session.commit()
from sqlalchemy.orm import sessionmaker as _sm
_TestSession = _sm(bind=db_engine, autoflush=False, autocommit=False)
call_count = 0
@@ -447,9 +470,7 @@ class TestBatchSummarize:
patch("app.services.summarizer.download_pdf", new_callable=AsyncMock),
patch("app.services.summarizer.call_pi", side_effect=_mock_call_pi),
):
result = await summarize_batch(
db_session, _session_factory=_TestSession
)
result = await summarize_batch(db_session, _session_factory=_TestSession)
assert result["done"] == 1
assert result["failed"] == 1
@@ -472,23 +493,32 @@ class TestBatchSummarize:
assert result["status"] == "conflict"
@pytest.mark.asyncio
async def test_task_lock_released(self, db_session, db_engine, mock_pi_output, _patch_paths):
async def test_task_lock_released(
self, db_session, db_engine, mock_pi_output, _patch_paths
):
"""完成后释放 TaskLock。"""
from sqlalchemy.orm import sessionmaker as _sm
_TestSession = _sm(bind=db_engine, autoflush=False, autocommit=False)
with (
patch("app.services.summarizer.download_pdf", new_callable=AsyncMock),
patch("app.services.summarizer.call_pi", new_callable=AsyncMock, return_value=mock_pi_output),
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value=mock_pi_output,
),
):
await summarize_batch(
db_session, _session_factory=_TestSession
)
await summarize_batch(db_session, _session_factory=_TestSession)
locks = db_session.query(TaskLock).filter(
TaskLock.task == "summarize",
TaskLock.lock_key == "batch",
).all()
locks = (
db_session.query(TaskLock)
.filter(
TaskLock.task == "summarize",
TaskLock.lock_key == "batch",
)
.all()
)
for lock in locks:
assert lock.status == "finished"
assert lock.released_at is not None