from datetime import UTC, datetime, timedelta from bs4 import BeautifulSoup from app.models import Comment, ContentItem, Hotspot, Report, Task from tests.helpers import make_test_client from sqlalchemy.exc import OperationalError def test_index_page_renders_task_form_empty_state_and_default_scale(): with make_test_client() as (client, _engine): response = client.get("/") assert response.status_code == 200 assert "热榜评论分析工具" in response.text assert 'name="platform"' in response.text assert 'name="hotspot_limit"' in response.text assert 'name="item_limit_per_hotspot"' in response.text assert 'name="comment_limit_per_item"' in response.text assert "1250" in response.text assert "手动刷新" not in response.text assert 'onclick="window.location.href=\'/\'"' not in response.text assert 'href="#create-task"' not in response.text assert 'href="#task-history"' not in response.text assert "还没有任何任务" in response.text def test_index_page_uses_pencil_control_console_layout_for_ui_polish(): with make_test_client() as (client, _engine): response = client.get("/") assert response.status_code == 200 soup = BeautifulSoup(response.text, "html.parser") visible_text = soup.get_text(" ") hero = soup.select_one(".control-hero") assert hero is not None assert "用默认配置快速启动一次评论分析" in visible_text assert "内容平台评论分析控制台" in visible_text assert soup.select_one(".quick-launch-panel") is not None assert soup.select_one(".default-scale-grid") is not None assert soup.select_one(".overview-grid") is not None assert soup.select_one(".task-card-list") is not None assert soup.select_one(".task-status-sidepanel") is not None assert soup.select_one("table.app-table.data-grid-table") is None assert "卡片化展示,减少表格压迫感" in visible_text assert "运行中任务自动刷新" in visible_text def test_index_page_lists_existing_tasks(): with make_test_client() as (client, engine): from sqlalchemy.orm import Session with Session(engine) as session: session.add( Task( id="task-visible", platform="douyin", status="running", hotspot_limit=3, item_limit_per_hotspot=4, comment_limit_per_item=50, total_items_count=4, processed_items_count=2, successful_items_count=1, failed_items_count=1, analysis_success_rate=0.75, analysis_status="insufficient", ) ) session.commit() response = client.get("/") assert response.status_code == 200 assert "task-visible" in response.text assert "抖音" in response.text assert "运行中" in response.text assert "已处理 2 / 共 4 条内容" in response.text assert "成功 1 / 失败 1" in response.text assert 'style="width: 50%"' in response.text assert "AI 成功率 75%" in response.text assert "AI 样本不足" in response.text assert 'data-task-list-auto-poll="true"' in response.text assert "pollTaskListStatus" in response.text assert "DOMContentLoaded" in response.text def test_index_page_uses_short_task_numbers_compact_fields_and_friendly_error_copy(): first_id = "11111111-1111-4111-8111-111111111111" second_id = "22222222-2222-4222-8222-222222222222" with make_test_client() as (client, engine): from sqlalchemy.orm import Session with Session(engine) as session: session.add( Task( id=first_id, platform="xiaohongshu", status="failed", created_at=datetime(2026, 7, 3, 10, 30, tzinfo=UTC), hotspot_limit=5, item_limit_per_hotspot=5, comment_limit_per_item=50, error_stage="system", error_type="unexpected_restart", error_message="系统重启,任务被中断", ) ) session.add( Task( id=second_id, platform="douyin", status="success", created_at=datetime(2026, 7, 3, 10, 35, tzinfo=UTC), hotspot_limit=1, item_limit_per_hotspot=1, comment_limit_per_item=10, ) ) session.commit() response = client.get("/") assert response.status_code == 200 visible_text = BeautifulSoup(response.text, "html.parser").get_text(" ") assert " 1 " in f" {visible_text} " assert " 2 " in f" {visible_text} " assert "#" not in visible_text assert first_id not in visible_text assert second_id not in visible_text assert "2026-07-03 18:30" in visible_text assert "2026-07-03 18:35" in visible_text assert "Demo 数据" not in visible_text assert "5热点 × 5内容 × 50评论" in visible_text assert "1热点 × 1内容 × 10评论" in visible_text assert "小红书 · 5热点" not in visible_text assert "抖音 · 1热点" not in visible_text assert "系统重启,任务被中断" in visible_text assert "system / unexpected_restart" not in visible_text assert "unexpected_restart" not in visible_text assert "内部演示工具 | 仅供学习参考" not in visible_text def test_index_page_does_not_auto_poll_without_running_tasks(): with make_test_client() as (client, engine): from sqlalchemy.orm import Session with Session(engine) as session: session.add( Task( id="task-done", platform="xiaohongshu", status="success", total_items_count=1, processed_items_count=1, successful_items_count=1, analysis_success_rate=1.0, ) ) session.commit() response = client.get("/") assert response.status_code == 200 assert "task-done" in response.text assert 'data-task-list-auto-poll="true"' not in response.text assert "pollTaskListStatus" not in response.text def test_running_task_detail_page_auto_polls_current_task(): with make_test_client() as (client, engine): from sqlalchemy.orm import Session with Session(engine) as session: session.add( Task( id="task-running", platform="xiaohongshu", status="running", hotspot_limit=1, item_limit_per_hotspot=1, comment_limit_per_item=10, total_items_count=2, processed_items_count=1, successful_items_count=1, failed_items_count=0, analysis_success_rate=1.0, ) ) session.commit() response = client.get("/tasks/task-running") assert response.status_code == 200 assert 'data-task-id="task-running"' in response.text assert "pollTaskDetailStatus" in response.text assert "手动刷新" not in response.text assert 'onclick="window.location.reload()"' not in response.text assert "已处理 1 / 共 2 条内容" in response.text assert "AI 成功率 100%" in response.text def test_task_detail_uses_short_number_title_and_hides_full_uuid(): task_id = "33333333-3333-4333-8333-333333333333" with make_test_client() as (client, engine): from sqlalchemy.orm import Session with Session(engine) as session: session.add( Task( id=task_id, platform="xiaohongshu", status="running", created_at=datetime(2026, 7, 3, 10, 30, tzinfo=UTC), hotspot_limit=5, item_limit_per_hotspot=5, comment_limit_per_item=50, ) ) session.commit() response = client.get(f"/tasks/{task_id}") assert response.status_code == 200 visible_text = BeautifulSoup(response.text, "html.parser").get_text(" ") assert "任务 1 - 热榜评论分析工具" in response.text assert "任务 1" in visible_text assert "完整 ID" not in visible_text assert task_id not in visible_text assert "任务 #33333333-3333-4333-8333-333333333333" not in visible_text assert "#" not in visible_text assert "创建时间 2026-07-03 18:30" in visible_text assert "Demo 数据" not in visible_text def test_demo_task_flag_is_kept_but_demo_copy_is_hidden_from_pages(): with make_test_client() as (client, engine): from sqlalchemy.orm import Session with Session(engine) as session: session.add( Task( id="demo-task-hidden-copy", platform="xiaohongshu", status="success", created_at=datetime(2026, 7, 3, 10, 30, tzinfo=UTC), hotspot_limit=1, item_limit_per_hotspot=1, comment_limit_per_item=10, ) ) session.commit() index_response = client.get("/") detail_response = client.get("/tasks/demo-task-hidden-copy") api_response = client.get("/api/tasks/demo-task-hidden-copy") assert index_response.status_code == 200 assert detail_response.status_code == 200 assert api_response.status_code == 200 assert api_response.json()["is_demo"] is True assert "Demo 数据" not in BeautifulSoup(index_response.text, "html.parser").get_text(" ") assert "Demo 数据" not in BeautifulSoup(detail_response.text, "html.parser").get_text(" ") def test_running_task_detail_page_keeps_polling_after_hotspots_exist(): with make_test_client() as (client, engine): from sqlalchemy.orm import Session with Session(engine) as session: task = Task( id="task-running-with-hotspot", platform="xiaohongshu", status="running", total_items_count=2, processed_items_count=1, successful_items_count=1, analysis_success_rate=1.0, ) session.add(task) session.add( Hotspot( id="hot-running", task_id=task.id, platform="xiaohongshu", title="运行中的热点", rank=1, raw_data="{}", ) ) session.commit() response = client.get("/tasks/task-running-with-hotspot") assert response.status_code == 200 assert "运行中的热点" in response.text assert "正在抓取热点数据" not in response.text assert "pollTaskDetailStatus" in response.text def test_running_task_detail_page_shows_stage_runtime_and_stale_progress_warning(monkeypatch): now = datetime(2026, 7, 3, 10, 30, tzinfo=UTC) monkeypatch.setattr("app.services.task_service.utc_now", lambda: now) with make_test_client() as (client, engine): from sqlalchemy.orm import Session with Session(engine) as session: task = Task( id="task-stale", platform="xiaohongshu", status="running", current_stage="ai_analysis", created_at=now - timedelta(hours=2, minutes=5), last_progress_at=now - timedelta(minutes=12), hotspot_limit=5, item_limit_per_hotspot=5, comment_limit_per_item=50, total_items_count=10, processed_items_count=6, successful_items_count=6, failed_items_count=0, analysis_success_rate=0.6, analysis_status="insufficient", ) session.add(task) hotspot = Hotspot(id="hot-stale", task_id=task.id, platform="xiaohongshu", title="运行热点", rank=1, raw_data="{}") session.add(hotspot) item = ContentItem( id="item-stale", task_id=task.id, hotspot_id=hotspot.id, platform="xiaohongshu", source_item_id="note-stale", item_type="note", title="运行内容", status="pending", raw_data="{}", ) session.add(item) for index in range(3): session.add( Comment( task_id=task.id, hotspot_id=hotspot.id, content_item_id=item.id, platform="xiaohongshu", source_comment_id=f"c{index}", content=f"评论 {index}", raw_data="{}", ) ) session.commit() response = client.get("/tasks/task-stale") assert response.status_code == 200 assert "阶段 AI 分析中" in response.text assert "运行时长" in response.text assert "2小时5分钟" in response.text assert "最近进度" in response.text assert "12分钟前" in response.text assert "已获取热点 1 / 目标 5" in response.text assert "评论 3" in response.text assert "超过 10 分钟没有进度更新" in response.text assert "可能仍在等待外部接口或 AI 响应" in response.text def test_failed_task_detail_page_shows_failure_reason_without_loading_copy(): with make_test_client() as (client, engine): from sqlalchemy.orm import Session with Session(engine) as session: session.add( Task( id="task-failed", platform="douyin", status="failed", error_stage="recover", error_type="interrupted", error_message="系统重启,任务被中断", ) ) session.commit() response = client.get("/tasks/task-failed") assert response.status_code == 200 assert "失败" in response.text assert "系统重启,任务被中断" in response.text assert "recover / interrupted" not in response.text assert "正在抓取热点数据" not in response.text assert "pollTaskDetailStatus" not in response.text def seed_result_data(engine): from sqlalchemy.orm import Session with Session(engine) as session: task = Task( id="task-result", platform="douyin", status="success", total_items_count=1, successful_items_count=1, analysis_success_rate=1.0, ) session.add(task) hotspot = Hotspot(id="hot-result", task_id=task.id, platform="douyin", title="热点标题", rank=1, raw_data="{}") session.add(hotspot) item = ContentItem( id="item-result", task_id=task.id, hotspot_id=hotspot.id, platform="douyin", source_item_id="v1", item_type="video", title="视频标题", status="success", raw_data="{}", ) session.add(item) session.add( Comment( id="comment-result", task_id=task.id, hotspot_id=hotspot.id, content_item_id=item.id, platform="douyin", source_comment_id="c1", content="评论内容", sentiment="positive", labels='["认可"]', like_count=3, raw_data="{}", ) ) session.add( Report( task_id=task.id, hotspot_id=hotspot.id, report_type="hotspot", title="热点标题", metrics_json='{"sample_count":1,"item_count":1,"sentiment":{"positive":{"count":1,"pct":100},"neutral":{"count":0,"pct":0},"negative":{"count":0,"pct":0},"unknown":{"count":0,"pct":0}},"top_labels":[{"name":"认可","count":1}]}', typical_comments_json='{"positive":[{"content":"评论内容","like_count":3}],"neutral":[],"negative":[]}', summary="热点总结", markdown_content="# 热点标题\n\n热点总结", data="{}", markdown="# 热点标题\n\n热点总结", ) ) session.add( Report( task_id=task.id, hotspot_id=hotspot.id, content_item_id=item.id, report_type="item", title="视频标题", metrics_json='{"sample_count":1,"sentiment":{"positive":{"count":1,"pct":100},"neutral":{"count":0,"pct":0},"negative":{"count":0,"pct":0},"unknown":{"count":0,"pct":0}},"top_labels":[{"name":"认可","count":1}]}', typical_comments_json='{"positive":[{"content":"评论内容","like_count":3}],"neutral":[],"negative":[]}', summary="内容总结", markdown_content="# 视频标题\n\n内容总结", data="{}", markdown="# 视频标题\n\n内容总结", ) ) session.commit() def test_result_pages_render_seeded_data(): with make_test_client() as (client, engine): seed_result_data(engine) task_response = client.get("/tasks/task-result") hotspot_response = client.get("/hotspots/hot-result/report") item_response = client.get("/items/item-result") assert task_response.status_code == 200 assert "热点标题" in task_response.text assert "热点 1:热点标题" in BeautifulSoup(task_response.text, "html.parser").get_text(" ") assert 'onclick="window.location.reload()"' not in task_response.text assert "手动刷新" not in task_response.text assert hotspot_response.status_code == 200 assert "热点总结" in hotspot_response.text assert "downloadExport(" not in hotspot_response.text assert "/api/export/" not in hotspot_response.text assert "导出 Markdown" not in hotspot_response.text assert "导出热点评论 CSV" not in hotspot_response.text assert "评论样本" in hotspot_response.text assert "关联内容" in hotspot_response.text assert "正向" in hotspot_response.text assert "1 条(100%)" in hotspot_response.text assert "认可 (1)" in hotspot_response.text assert "典型评论" in hotspot_response.text assert "评论内容" in hotspot_response.text assert "