feat: enhance UI, refactor services, improve templates and tests

- Replace image_extractor with pdf_image_extractor service
- Enhance pi_client with expanded API capabilities
- Improve summarizer service with additional features
- Update admin routes with more endpoints
- Add login page template
- Enhance detail page with comprehensive layout
- Improve search and trends pages
- Update base template with additional elements
- Refactor tests for better coverage
- Add validate_summary script
- Update project configuration and dependencies
This commit is contained in:
2026-06-07 19:38:58 +08:00
parent 4a72c35452
commit 0d293422ac
32 changed files with 2003 additions and 586 deletions
+18 -26
View File
@@ -182,7 +182,7 @@ class TestSummarizeOneFlow:
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value=mock_pi_output,
return_value=(mock_pi_output, "test-session-id"),
),
):
result = await summarize_one(db_session, sample_paper)
@@ -246,27 +246,28 @@ class TestSummarizeOneFlow:
@pytest.mark.asyncio
async def test_json_not_found(self, db_session, sample_paper, _patch_paths):
"""pi 输出无 JSON → json_not_found"""
"""pi 输出无 JSON → 验证循环重试 4 次后 ValueError (unknown)"""
with (
patch("app.services.summarizer.download_pdf", new_callable=AsyncMock),
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value="No JSON in this output at all.",
return_value=("No JSON in this output at all.", "test-session-id"),
),
):
result = await summarize_one(db_session, sample_paper)
assert result["status"] == "failed"
assert result["error_type"] == "json_not_found"
assert result["error_type"] == "unknown"
@pytest.mark.asyncio
async def test_field_missing_and_retry(
async def test_validation_fails_and_retries(
self, db_session, sample_paper, _patch_paths
):
"""必填字段缺失 → field_missing → retry → permanent_failure"""
"""验证失败(字段不符合要求)→ 重试多次后失败"""
bad_json = json.dumps(
{
"arxiv_id": sample_paper.arxiv_id,
"title_zh": "", # 空的必填字段
"one_line": "valid line",
"tags": ["tag1"],
@@ -282,23 +283,14 @@ class TestSummarizeOneFlow:
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value=bad_output,
return_value=(bad_output, "test-session-id"),
),
):
# 第一次失败 → pending (retry)
result1 = await summarize_one(db_session, sample_paper)
assert result1["status"] == "failed"
assert result1["error_type"] == "field_missing"
assert result1["retry_count"] == 1
# 第二次失败 → permanent_failure (SUMMARY_MAX_RETRIES=1, 所以 2 次 > 1+1)
db_session.refresh(sample_paper)
result2 = await summarize_one(db_session, sample_paper)
assert result2["status"] == "failed"
assert result2["retry_count"] == 2
db_session.refresh(sample_paper)
assert sample_paper.summary_status.status == "permanent_failure"
# _validate_summary 先拦截,4 轮都失败后 ValueError → unknown
result = await summarize_one(db_session, sample_paper)
assert result["status"] == "failed"
assert result["error_type"] == "unknown"
assert result["retry_count"] == 1
@pytest.mark.asyncio
async def test_raw_output_saved_on_failure(
@@ -310,7 +302,7 @@ class TestSummarizeOneFlow:
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value="Some output without JSON",
return_value=("Some output without JSON", "test-session-id"),
),
):
await summarize_one(db_session, sample_paper)
@@ -329,7 +321,7 @@ class TestSummarizeOneFlow:
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value=mock_pi_output,
return_value=(mock_pi_output, "test-session-id"),
),
):
await summarize_one(db_session, sample_paper)
@@ -417,7 +409,7 @@ class TestBatchSummarize:
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value=mock_pi_output,
return_value=(mock_pi_output, "test-session-id"),
),
):
result = await summarize_batch(db_session, _session_factory=_TestSession)
@@ -464,7 +456,7 @@ class TestBatchSummarize:
call_count += 1
if call_count == 1:
raise PiTimeoutError("timeout")
return mock_pi_output
return mock_pi_output, "test-session-id"
with (
patch("app.services.summarizer.download_pdf", new_callable=AsyncMock),
@@ -506,7 +498,7 @@ class TestBatchSummarize:
patch(
"app.services.summarizer.call_pi",
new_callable=AsyncMock,
return_value=mock_pi_output,
return_value=(mock_pi_output, "test-session-id"),
),
):
await summarize_batch(db_session, _session_factory=_TestSession)