diff --git a/docs/MVP-WorkOrders.md b/docs/MVP-WorkOrders.md index d0d2160..7fa9b45 100644 --- a/docs/MVP-WorkOrders.md +++ b/docs/MVP-WorkOrders.md @@ -978,6 +978,26 @@ fix: 修复页面刷新和按钮点击的 500 错误 - 用户需要提供或复现最容易触发 500 的页面 URL 与按钮名称;如果无法提供,则开发时先从当前浏览器打开的任务详情页开始排查。 +完成记录: + +- 完成日期:2026-07-03 +- 相关改动: + - 补充路由回归测试,覆盖任务详情页、热点报告页、内容详情页、导出接口在数据库 I/O error 下的 503 兜底。 + - 补充缺失任务、缺失热点、缺失内容页面的 404 兜底测试,确保不是裸 500。 + - 核查当前实现已有 `OperationalError` / `SQLAlchemyError` 全局 handler:API 返回 503 JSON,HTML 页面返回数据库不可用友好页。 +- 验证命令: + - `docker cp tests/. hot-comments-tool-app-1:/app/tests/` + - `docker exec hot-comments-tool-app-1 sh -lc 'python -m pytest /app/tests/integration/test_routes.py -q'` +- 验证结果: + - `19 passed, 1 warning` +- 验收结论: + - 已知高频页面路由和导出入口在数据库 I/O error 下不会返回裸 `Internal Server Error`。 + - 缺失任务、热点、内容不会返回 500。 + - 服务端日志仍保留异常信息,页面/API 返回可理解错误。 +- 遗留问题: + - 轮询接口偶发 503 时,前端当前只是静默返回,用户看不到“最近一次同步失败”提示;归入 WO-19 / WO-13 前端提示优化。 + - 当前本机 Python `.venv` 仍未完成依赖安装,本轮测试通过运行容器执行。 + ### WO-12 导出 Markdown / CSV 点击失效修复 优先级:P0 diff --git a/tests/integration/test_routes.py b/tests/integration/test_routes.py index 8d599d3..c892da6 100644 --- a/tests/integration/test_routes.py +++ b/tests/integration/test_routes.py @@ -384,6 +384,102 @@ def test_index_page_shows_database_error_state_when_database_has_io_error(monkey assert "请先保留 data 目录" in response.text +def test_task_detail_page_shows_database_error_state_when_database_has_io_error(monkeypatch): + def broken_get_task(_session, _task_id): + raise OperationalError("SELECT 1", {}, Exception("disk I/O error")) + + monkeypatch.setattr("app.main.get_task", broken_get_task) + + with make_test_client() as (client, _engine): + response = client.get("/tasks/task-io-error") + + assert response.status_code == 503 + assert "数据库暂时不可用" in response.text + assert "请先保留 data 目录" in response.text + + +def test_hotspot_report_page_shows_database_error_state_when_database_has_io_error(monkeypatch): + def broken_get_task(_session, _task_id): + raise OperationalError("SELECT 1", {}, Exception("disk I/O error")) + + monkeypatch.setattr("app.main.get_task", broken_get_task) + + with make_test_client() as (client, engine): + from sqlalchemy.orm import Session + + with Session(engine) as session: + session.add(Task(id="task-hot-io", platform="xiaohongshu", status="success")) + session.add(Hotspot(id="hot-io", task_id="task-hot-io", platform="xiaohongshu", title="热点", rank=1, raw_data="{}")) + session.commit() + + response = client.get("/hotspots/hot-io/report") + + assert response.status_code == 503 + assert "数据库暂时不可用" in response.text + + +def test_item_detail_page_shows_database_error_state_when_database_has_io_error(monkeypatch): + def broken_get_task(_session, _task_id): + raise OperationalError("SELECT 1", {}, Exception("disk I/O error")) + + monkeypatch.setattr("app.main.get_task", broken_get_task) + + with make_test_client() as (client, engine): + from sqlalchemy.orm import Session + + with Session(engine) as session: + session.add(Task(id="task-item-io", platform="xiaohongshu", status="success")) + session.add(Hotspot(id="hot-item-io", task_id="task-item-io", platform="xiaohongshu", title="热点", rank=1, raw_data="{}")) + session.add( + ContentItem( + id="item-io", + task_id="task-item-io", + hotspot_id="hot-item-io", + platform="xiaohongshu", + source_item_id="item-io-source", + item_type="note", + status="success", + raw_data="{}", + ) + ) + session.commit() + + response = client.get("/items/item-io") + + assert response.status_code == 503 + assert "数据库暂时不可用" in response.text + + +def test_export_route_returns_service_unavailable_when_database_has_io_error(monkeypatch): + def broken_export(_session, _item_id): + raise OperationalError("SELECT 1", {}, Exception("disk I/O error")) + + monkeypatch.setattr("app.main.export_item_comments_csv", broken_export) + + with make_test_client() as (client, _engine): + response = client.get("/api/export/items/item-io/comments.csv") + + assert response.status_code == 503 + assert response.json()["detail"] == "数据库暂时不可用,请稍后重试或联系维护者恢复数据。" + + +def test_missing_html_pages_render_friendly_not_found_page(): + with make_test_client() as (client, _engine): + task_response = client.get("/tasks/missing-task") + hotspot_response = client.get("/hotspots/missing-hotspot/report") + item_response = client.get("/items/missing-item") + + assert task_response.status_code == 404 + assert "任务不存在" in task_response.text + assert "Internal Server Error" not in task_response.text + assert hotspot_response.status_code == 404 + assert "热点不存在" in hotspot_response.text + assert "Internal Server Error" not in hotspot_response.text + assert item_response.status_code == 404 + assert "内容条目不存在" in item_response.text + assert "Internal Server Error" not in item_response.text + + def test_task_api_includes_progress_counts_stage_and_demo_flag(): with make_test_client() as (client, engine): seed_result_data(engine)