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 from tests.helpers import make_test_client class FailingHotspotPlatform: def fetch_hotspots(self, *, limit): 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()) with Session(engine) as session: task = create_task( session, CreateTaskRequest(platform="douyin", hotspot_limit=1, item_limit_per_hotspot=1, comment_limit_per_item=10), submit_background=False, ) task_id = task.id run_task(task_id, session_factory=lambda: session) task = session.get(type(task), task_id) assert task.status == "failed" assert task.error_stage == "crawl_hotspots" assert task.error_type == "api_error" def test_unexpected_task_exception_marks_task_failed(monkeypatch): def raise_unexpected_error(_platform): raise RuntimeError("boom") with make_test_client() as (_client, engine): monkeypatch.setattr("app.services.task_service.build_platform", raise_unexpected_error) with Session(engine) as session: task = create_task( session, CreateTaskRequest(platform="douyin", hotspot_limit=1, item_limit_per_hotspot=1, comment_limit_per_item=10), submit_background=False, ) task_id = task.id run_task(task_id, session_factory=lambda: session) task = session.get(type(task), task_id) assert task.status == "failed" 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"