diff --git a/docs/MVP-WorkOrders.md b/docs/MVP-WorkOrders.md index 4f64eca..d5636c4 100644 --- a/docs/MVP-WorkOrders.md +++ b/docs/MVP-WorkOrders.md @@ -345,6 +345,28 @@ feat: 打磨报告页和内容详情页 test: 记录双平台小规模真实验收 ``` +完成记录: + +```text +完成日期:2026-07-03 +相关 commit:test: 记录双平台小规模真实验收 +验证命令: +- .venv/bin/python -m pytest tests/unit tests/integration -q +- .venv/bin/python -m pytest tests/unit tests/integration --cov=app --cov-branch --cov-report=term-missing +- docker compose up -d --build +- curl -f http://localhost:8000/health +真实任务 ID: +- 小红书:8ae106f4-64dc-4675-a021-f78092926ce6 +- 抖音:b295f4b2-d546-4121-99bc-48d166d86bf4 +验收结论: +- 小红书 1×1×10:status=success,热点 1,内容 1,评论 10,报告 2,AI 成功率 100%。 +- 抖音 1×1×10:status=success,热点 1,内容 1,评论 10,报告 2,AI 成功率 100%。 +- 页面可查看任务详情、热点报告、内容详情和评论明细。 +- 自动化测试补充了连续 429 的 1s→2s→4s 退避断言,以及单个内容条目失败后继续处理后续条目的容错测试。 +- 代码中未发现 httpx.AsyncClient 使用。 +遗留问题:无;进入 WO-04 默认规模验收。 +``` + ### WO-04 双平台默认规模验收 优先级:P0 diff --git a/tests/integration/test_failure_tolerance.py b/tests/integration/test_failure_tolerance.py index 146eaf5..c27e73a 100644 --- a/tests/integration/test_failure_tolerance.py +++ b/tests/integration/test_failure_tolerance.py @@ -1,5 +1,6 @@ from sqlalchemy.orm import Session +from app.models import ContentItem, Task from app.platforms.base import PlatformAPIError from app.schemas import CreateTaskRequest from app.services.task_service import create_task, run_task @@ -11,6 +12,28 @@ class FailingHotspotPlatform: raise PlatformAPIError("hotspot failed", error_type="api_error", status_code=500) +class OneFailedOneSuccessfulItemPlatform: + def fetch_hotspots(self, *, limit): + from app.platforms.base import HotspotData + + return [HotspotData(source_hot_id="h1", title="热点一", rank=1)] + + def search_items_by_hotspot(self, keyword, *, limit): + from app.platforms.base import ContentItemData + + return [ + ContentItemData(source_item_id="bad-item", item_type="video", title="失败内容"), + ContentItemData(source_item_id="good-item", item_type="video", title="成功内容"), + ] + + def fetch_comments(self, source_item_id, *, limit): + from app.platforms.base import CommentData + + if source_item_id == "bad-item": + raise PlatformAPIError("comments failed", error_type="rate_limited", status_code=429) + return [CommentData(source_comment_id="c1", content="继续成功")] + + def test_hotspot_failure_marks_task_failed(monkeypatch): with make_test_client() as (_client, engine): monkeypatch.setattr("app.services.task_service.build_platform", lambda _platform: FailingHotspotPlatform()) @@ -49,3 +72,33 @@ def test_unexpected_task_exception_marks_task_failed(monkeypatch): assert task.error_stage == "system" assert task.error_type == "unexpected_error" assert task.error_message == "boom" + + +def test_failed_content_item_is_recorded_and_following_item_continues(monkeypatch): + with make_test_client() as (_client, engine): + monkeypatch.setattr("app.services.task_service.build_platform", lambda _platform: OneFailedOneSuccessfulItemPlatform()) + monkeypatch.setattr("app.services.task_service.build_ai_dependencies", lambda: (lambda _prompt: "[]", None)) + monkeypatch.setattr("app.services.ai_service.time.sleep", lambda _seconds: None) + + with Session(engine) as session: + task = create_task( + session, + CreateTaskRequest(platform="douyin", hotspot_limit=1, item_limit_per_hotspot=2, comment_limit_per_item=10), + submit_background=False, + ) + task_id = task.id + run_task(task_id, session_factory=lambda: session) + + persisted = session.get(Task, task_id) + failed_item = session.query(ContentItem).filter_by(task_id=task_id, source_item_id="bad-item").one() + successful_item = session.query(ContentItem).filter_by(task_id=task_id, source_item_id="good-item").one() + + assert persisted.status == "success" + assert persisted.total_items_count == 2 + assert persisted.processed_items_count == 2 + assert persisted.successful_items_count == 1 + assert persisted.failed_items_count == 1 + assert failed_item.status == "failed" + assert failed_item.error_stage == "crawl_comments" + assert failed_item.error_type == "rate_limited" + assert successful_item.status == "success" diff --git a/tests/unit/test_comment_pagination.py b/tests/unit/test_comment_pagination.py index d5132fd..2bf6179 100644 --- a/tests/unit/test_comment_pagination.py +++ b/tests/unit/test_comment_pagination.py @@ -38,7 +38,8 @@ def test_tikhub_client_retries_429_with_exponential_backoff(monkeypatch): def test_tikhub_client_raises_structured_error_after_retries(monkeypatch): - monkeypatch.setattr("app.platforms.base.time.sleep", lambda _seconds: None) + sleeps = [] + monkeypatch.setattr("app.platforms.base.time.sleep", sleeps.append) transport = SequenceTransport([httpx.Response(429, json={"message": "Too Many Requests"}) for _ in range(4)]) http_client = httpx.Client(transport=httpx.MockTransport(transport)) client = TikHubClient(base_url="https://api.test", api_key="secret-token", http_client=http_client) @@ -49,3 +50,4 @@ def test_tikhub_client_raises_structured_error_after_retries(monkeypatch): assert exc_info.value.error_type == "rate_limited" assert exc_info.value.status_code == 429 assert "secret-token" not in str(exc_info.value) + assert sleeps == [1, 2, 4]