""" 一键生成链路日志最小集 单元测试 覆盖: - GenerationTask.append_log() 正确追加结构化日志 - GenerationTask.append_log() 超过 200 条时截断 - GenerationTask.get_logs() 正确解析 JSON - GenerationTask.get_logs() 异常 JSON 不抛异常 - GenerationTaskResponse logs 字段 validator 解析 JSON 字符串 - GenerationTaskResponse logs 字段 validator 处理非法输入 - Worker 日志格式 [task_id=xxx] [阶段] 消息 - _flush_logs 异常不抛出 """ from __future__ import annotations import json import sys from pathlib import Path from unittest.mock import MagicMock, patch sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "worker")) # ── 预注入 mock 模块,防止 worker_app.db 触发真实数据库连接 ── _mock_db_module = MagicMock() _mock_db_module.SessionLocal = MagicMock() sys.modules.setdefault("worker_app.db", _mock_db_module) if "worker_app" in sys.modules: sys.modules["worker_app"].db = _mock_db_module from app.schemas.generation_task import GenerationTaskResponse from packages.domain.generation_task import GenerationTask, GenerationTaskStatus def _make_task(**kwargs) -> GenerationTask: """创建测试用 GenerationTask。""" defaults = { "id": "task-001", "project_id": "proj-001", "asset_library_id": "lib-001", "strategy_id": "one_take", "voice_library_id": "", "template_id": "tpl-001", "asset_ids": ["asset-1", "asset-2"], "title_ids": [], "voice_ids": [], "status": GenerationTaskStatus.PENDING, "progress": 0.0, "result_count": 0, "error_message": "", "created_by_user_id": "user-001", "source_edit_plan_id": "", "asset_select_mode": "all", "batch_id": "", } defaults.update(kwargs) return GenerationTask(**defaults) class TestAppendLog: """GenerationTask.append_log() 单元测试。""" def test_append_single_log(self): task = _make_task() task.append_log("接收任务", "任务开始", mode="one_take") logs = task.get_logs() assert len(logs) == 1 entry = logs[0] assert entry["level"] == "INFO" assert entry["stage"] == "接收任务" assert entry["message"] == "任务开始" assert entry["mode"] == "one_take" assert "ts" in entry def test_append_multiple_logs(self): task = _make_task() task.append_log("接收任务", "任务开始") task.append_log("下载素材", "下载完成", count=3) task.append_log("渲染", "渲染完成", duration=12.5) logs = task.get_logs() assert len(logs) == 3 assert logs[0]["stage"] == "接收任务" assert logs[1]["stage"] == "下载素材" assert logs[1]["count"] == 3 assert logs[2]["stage"] == "渲染" assert logs[2]["duration"] == 12.5 def test_append_log_with_error_level(self): task = _make_task() task.append_log("任务失败", "OSS上传失败", level="ERROR", error_type="RuntimeError") logs = task.get_logs() assert len(logs) == 1 assert logs[0]["level"] == "ERROR" assert logs[0]["error_type"] == "RuntimeError" def test_append_log_truncates_at_200(self): task = _make_task() for i in range(250): task.append_log("阶段", f"消息{i}") logs = task.get_logs() assert len(logs) == 200 # 保留最后 200 条 assert logs[0]["message"] == "消息50" assert logs[-1]["message"] == "消息249" def test_append_log_handles_corrupted_json(self): task = _make_task(logs="not-valid-json") task.append_log("接收任务", "任务开始") logs = task.get_logs() assert len(logs) == 1 assert logs[0]["message"] == "任务开始" def test_append_log_handles_empty_string(self): task = _make_task(logs="") task.append_log("接收任务", "任务开始") logs = task.get_logs() assert len(logs) == 1 class TestGetLogs: """GenerationTask.get_logs() 单元测试。""" def test_get_logs_empty(self): task = _make_task() assert task.get_logs() == [] def test_get_logs_parses_json(self): entries = [{"ts": "2026-01-01T00:00:00Z", "level": "INFO", "stage": "test", "message": "hello"}] task = _make_task(logs=json.dumps(entries, ensure_ascii=False)) logs = task.get_logs() assert len(logs) == 1 assert logs[0]["message"] == "hello" def test_get_logs_handles_invalid_json(self): task = _make_task(logs="{broken") assert task.get_logs() == [] def test_get_logs_handles_none(self): task = _make_task(logs=None) assert task.get_logs() == [] class TestGenerationTaskResponseLogs: """GenerationTaskResponse logs 字段 validator 测试。""" def _make_response_data(self, logs_value) -> dict: return { "id": "task-001", "project_id": "proj-001", "asset_library_id": "lib-001", "strategy_id": "one_take", "voice_library_id": "", "template_id": "", "asset_ids": [], "title_ids": [], "voice_ids": [], "source_edit_plan_id": "", "asset_select_mode": "all", "batch_id": "", "status": "completed", "progress": 1.0, "result_count": 1, "error_message": "", "logs": logs_value, } def test_logs_json_string_parsed(self): entries = [{"ts": "2026-01-01T00:00:00Z", "level": "INFO", "stage": "test", "message": "ok"}] data = self._make_response_data(json.dumps(entries, ensure_ascii=False)) resp = GenerationTaskResponse(**data) assert isinstance(resp.logs, list) assert len(resp.logs) == 1 assert resp.logs[0]["message"] == "ok" def test_logs_list_passthrough(self): entries = [{"ts": "2026-01-01T00:00:00Z", "level": "INFO", "stage": "test", "message": "ok"}] data = self._make_response_data(entries) resp = GenerationTaskResponse(**data) assert resp.logs == entries def test_logs_invalid_json_returns_empty(self): data = self._make_response_data("{broken") resp = GenerationTaskResponse(**data) assert resp.logs == [] def test_logs_empty_string_returns_empty(self): data = self._make_response_data("") resp = GenerationTaskResponse(**data) assert resp.logs == [] def test_logs_default_empty(self): data = self._make_response_data("[]") resp = GenerationTaskResponse(**data) assert resp.logs == [] class TestWorkerLogFormat: """Worker 日志格式 [task_id=xxx] [阶段] 消息 测试。""" def test_log_format_pattern(self): """验证日志格式匹配 [task_id=xxx] [阶段] 消息。""" import re task_id = "abc123" stage = "下载素材" message = "完成: 成功=3个, 耗时=1.5s" formatted = f"[task_id={task_id}] [{stage}] {message}" pattern = r"^\[task_id=[\w-]+\] \[.+\] .+$" assert re.match(pattern, formatted) def test_log_entries_contain_required_fields(self): """验证 append_log 生成的条目包含所有必需字段。""" task = _make_task() task.append_log("OSS上传", "上传成功", file_size=1024000, duration=2.5) logs = task.get_logs() entry = logs[0] assert "ts" in entry assert "level" in entry assert "stage" in entry assert "message" in entry assert entry["file_size"] == 1024000 assert entry["duration"] == 2.5 class TestFlushLogs: """_flush_logs 异常安全测试。""" def test_flush_logs_exception_not_raised(self): """_flush_logs 在 DB 异常时不应抛出。""" # 模拟 worker 环境 sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "worker")) from worker_app.tasks.generation import _flush_logs task = _make_task() task.append_log("测试", "消息") # Mock SessionLocal 抛异常 with patch("worker_app.tasks.generation.SessionLocal", side_effect=RuntimeError("DB error")): # 不应抛出 _flush_logs("task-001", task)