1a57878f76
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1m12s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1m33s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m14s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
1. 未使用依赖清理:
- 从 requirements-base.txt 移除 cryptography 和 pyOpenSSL
2. pyflakes 警告清零 (apps/ + packages/ + tests/):
- 移除 17 处未使用的 import (F401)
- 修复 26 处未使用的局部变量 (F841):
* 有副作用的赋值转为裸调用
* 无副作用的赋值直接删除
- 修复 1 处未使用的异常变量 (F841)
- 修复 1 处空 except 块
3. 测试文件冗余清理:
- 删除 tests/integration/test_project_management.py (模块级 skip,测试不存在的模块)
- 删除 tests/integration/fixtures/duplication_routes_fixed.py (未被引用)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
215 lines
8.4 KiB
Python
215 lines
8.4 KiB
Python
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# 设置必要环境变量(必须在导入 app 模块之前)
|
|
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret-key-for-testing")
|
|
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
|
|
|
|
from app.api.routes.asset_diagnosis import _build_diagnosis
|
|
from fastapi import FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from packages.domain import (
|
|
Asset,
|
|
AssetLibrary,
|
|
AssetLibraryKind,
|
|
AssetStatus,
|
|
ClassificationStatus,
|
|
Project,
|
|
)
|
|
|
|
|
|
def _asset(name: str, mime_type: str, *, status=AssetStatus.READY, duration=None, quality_score=None):
|
|
return Asset.create(
|
|
project_id="project-1",
|
|
library_id="library-1",
|
|
name=name,
|
|
storage_key=f"uploads/{name}",
|
|
mime_type=mime_type,
|
|
file_size=1024,
|
|
duration=duration,
|
|
status=status,
|
|
classification_status=ClassificationStatus.COMPLETED,
|
|
quality_score=quality_score,
|
|
)
|
|
|
|
|
|
def test_asset_diagnosis_reports_missing_video_gap():
|
|
diagnosis = _build_diagnosis(
|
|
"project-1",
|
|
[_asset("voice.mp3", "audio/mpeg"), _asset("image.jpg", "image/jpeg")],
|
|
)
|
|
|
|
assert diagnosis.readiness_score < 80
|
|
assert diagnosis.video_assets == 0
|
|
assert any(gap.key == "missing_video" and gap.severity == "critical" for gap in diagnosis.gaps)
|
|
|
|
|
|
def test_asset_diagnosis_scores_ready_video_assets():
|
|
used_asset = _asset("video-1.mp4", "video/mp4", duration=8)
|
|
used_asset.metadata = {"generation_use_count": 1, "review_status": "pending_review"}
|
|
diagnosis = _build_diagnosis(
|
|
"project-1",
|
|
[
|
|
used_asset,
|
|
_asset("video-2.mp4", "video/mp4", duration=8),
|
|
_asset("video-3.mov", "video/quicktime", duration=8),
|
|
_asset("voice.mp3", "audio/mpeg"),
|
|
],
|
|
)
|
|
|
|
assert diagnosis.readiness_score >= 80
|
|
assert diagnosis.video_assets == 3
|
|
assert diagnosis.voice_assets == 1
|
|
assert diagnosis.estimated_video_count == 3
|
|
assert diagnosis.used_assets == 1
|
|
assert diagnosis.unused_assets == 3
|
|
assert diagnosis.pending_review_assets == 1
|
|
smart_view_counts = {item.key: item.count for item in diagnosis.smart_views}
|
|
assert smart_view_counts["recommended"] == 3
|
|
assert smart_view_counts["used"] == 1
|
|
assert smart_view_counts["pending_review"] == 1
|
|
|
|
|
|
def test_asset_diagnosis_flags_unready_and_low_quality_assets():
|
|
diagnosis = _build_diagnosis(
|
|
"project-1",
|
|
[
|
|
_asset("video.mp4", "video/mp4", duration=10, quality_score=40),
|
|
_asset("pending.mp4", "video/mp4", status=AssetStatus.UPLOADING),
|
|
],
|
|
)
|
|
|
|
gap_keys = {gap.key for gap in diagnosis.gaps}
|
|
assert "not_ready_assets" in gap_keys
|
|
assert "low_quality_assets" in gap_keys
|
|
smart_view_counts = {item.key: item.count for item in diagnosis.smart_views}
|
|
assert smart_view_counts["needs_attention"] == 2
|
|
assert smart_view_counts["high_risk"] == 1
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 路由层测试 — 验证 find_by_project 调用正确性
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _StubProjectRepository:
|
|
def __init__(self, projects: dict[str, Project] | None = None):
|
|
self._projects = projects or {}
|
|
|
|
def get(self, project_id: str) -> Project | None:
|
|
return self._projects.get(project_id)
|
|
|
|
def find_by_id(self, project_id: str) -> Project | None:
|
|
return self._projects.get(project_id)
|
|
|
|
|
|
class _StubAssetLibraryRepository:
|
|
def __init__(self, libraries: dict[str, AssetLibrary] | None = None):
|
|
self._libraries = libraries or {}
|
|
self.find_by_project_called_with: list[str] = []
|
|
|
|
def find_by_project(self, project_id: str, kind=None) -> list[AssetLibrary]:
|
|
self.find_by_project_called_with.append(project_id)
|
|
return [lib for lib in self._libraries.values() if lib.project_id == project_id]
|
|
|
|
def list_by_project(self, project_id: str) -> list[AssetLibrary]:
|
|
raise AssertionError("路由不应调用 list_by_project,应调用 find_by_project")
|
|
|
|
|
|
class _StubAssetRepository:
|
|
def __init__(self, assets: dict[str, Asset] | None = None):
|
|
self._assets = assets or {}
|
|
self.list_by_library_called_with: list[str] = []
|
|
|
|
def list_by_library(self, library_id: str, skip: int = 0, limit: int = 50) -> list[Asset]:
|
|
self.list_by_library_called_with.append(library_id)
|
|
return [a for a in self._assets.values() if a.library_id == library_id]
|
|
|
|
def count_by_library(self, library_id: str) -> int:
|
|
return len([a for a in self._assets.values() if a.library_id == library_id])
|
|
|
|
|
|
def _dep(name: str):
|
|
from app import dependencies
|
|
|
|
return getattr(dependencies, name)
|
|
|
|
|
|
def _build_route_test_app(project_repo, library_repo, asset_repo):
|
|
from unittest.mock import MagicMock
|
|
|
|
from app.api.routes.asset_diagnosis import router
|
|
from app.auth import AuthenticatedUser, get_current_user
|
|
|
|
app = FastAPI()
|
|
app.include_router(router, prefix="/api/v1")
|
|
|
|
mock_user = MagicMock(spec=AuthenticatedUser)
|
|
mock_user.id = "user-1"
|
|
mock_user.email = "test@example.com"
|
|
mock_user.user.id = "user-1"
|
|
app.dependency_overrides[get_current_user] = lambda: mock_user
|
|
app.dependency_overrides[_dep("get_project_repository")] = lambda: project_repo
|
|
app.dependency_overrides[_dep("get_asset_library_repository")] = lambda: library_repo
|
|
app.dependency_overrides[_dep("get_asset_repository")] = lambda: asset_repo
|
|
return app
|
|
|
|
|
|
class TestAssetDiagnosisRoute:
|
|
"""路由层测试 — 验证 find_by_project 调用正确性。"""
|
|
|
|
def test_returns_404_when_project_not_found(self):
|
|
project_repo = _StubProjectRepository()
|
|
library_repo = _StubAssetLibraryRepository()
|
|
asset_repo = _StubAssetRepository()
|
|
app = _build_route_test_app(project_repo, library_repo, asset_repo)
|
|
client = TestClient(app)
|
|
|
|
resp = client.get("/api/v1/projects/nonexistent/asset-diagnosis")
|
|
assert resp.status_code == 404
|
|
|
|
def test_find_by_project_called_with_correct_project_id(self):
|
|
project = Project(id="proj-123", name="Test", owner_user_id="user-1")
|
|
library = AssetLibrary(id="lib-1", name="Lib", project_id="proj-123", kind=AssetLibraryKind.VIDEO)
|
|
project_repo = _StubProjectRepository({"proj-123": project})
|
|
library_repo = _StubAssetLibraryRepository({"lib-1": library})
|
|
asset_repo = _StubAssetRepository()
|
|
app = _build_route_test_app(project_repo, library_repo, asset_repo)
|
|
client = TestClient(app)
|
|
|
|
resp = client.get("/api/v1/projects/proj-123/asset-diagnosis")
|
|
assert resp.status_code == 200
|
|
assert library_repo.find_by_project_called_with == ["proj-123"]
|
|
|
|
def test_list_by_library_called_for_each_library(self):
|
|
project = Project(id="proj-1", name="Test", owner_user_id="user-1")
|
|
lib1 = AssetLibrary(id="lib-1", name="Lib1", project_id="proj-1", kind=AssetLibraryKind.VIDEO)
|
|
lib2 = AssetLibrary(id="lib-2", name="Lib2", project_id="proj-1", kind=AssetLibraryKind.VOICE)
|
|
project_repo = _StubProjectRepository({"proj-1": project})
|
|
library_repo = _StubAssetLibraryRepository({"lib-1": lib1, "lib-2": lib2})
|
|
asset_repo = _StubAssetRepository()
|
|
app = _build_route_test_app(project_repo, library_repo, asset_repo)
|
|
client = TestClient(app)
|
|
|
|
resp = client.get("/api/v1/projects/proj-1/asset-diagnosis")
|
|
assert resp.status_code == 200
|
|
assert set(asset_repo.list_by_library_called_with) == {"lib-1", "lib-2"}
|
|
|
|
def test_find_by_project_not_list_by_project(self):
|
|
"""路由调用 find_by_project 而非 list_by_project(否则会触发 AssertionError)。"""
|
|
project = Project(id="proj-1", name="Test", owner_user_id="user-1")
|
|
library = AssetLibrary(id="lib-1", name="Lib", project_id="proj-1", kind=AssetLibraryKind.VIDEO)
|
|
project_repo = _StubProjectRepository({"proj-1": project})
|
|
library_repo = _StubAssetLibraryRepository({"lib-1": library})
|
|
asset_repo = _StubAssetRepository()
|
|
app = _build_route_test_app(project_repo, library_repo, asset_repo)
|
|
client = TestClient(app)
|
|
|
|
resp = client.get("/api/v1/projects/proj-1/asset-diagnosis")
|
|
# 如果调用了 list_by_project,会抛 AssertionError → 500
|
|
assert resp.status_code == 200
|