fix: 查重流程全面核实修复 — 两阶段持久化 + 重新计算查重API (#1664) (#1684)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 18s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 16s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 33s
CI/CD Pipeline / Build Staging API Image (push) Successful in 26s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 40s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m54s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m7s
AI Code Review / AI Code Review (pull_request) Failing after 2m25s
CI/CD Pipeline / Integration Tests (push) Successful in 2m28s
CI/CD Pipeline / Validate - Style (push) Successful in 2m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m1s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m34s
CI/CD Pipeline / Validate - Security (push) Successful in 5m2s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m8s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m56s
CI/CD Pipeline / Unit Tests (push) Successful in 8m40s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 290h15m45s
CI/CD Pipeline / Deploy Production (push) Failing after 290h15m45s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 290h15m48s
CI/CD Pipeline / Build Production Web Image (push) Failing after 290h15m48s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 290h21m26s
CI/CD Pipeline / Build Production API Image (push) Failing after 290h15m48s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 290h22m39s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 290h22m39s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 290h22m39s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 290h22m40s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 290h22m43s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 290h22m43s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 290h22m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 290h22m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 290h23m22s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 290h23m23s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 290h23m55s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 290h23m55s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 290h24m23s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 290h24m23s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 290h24m24s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 290h24m24s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 290h24m24s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 290h24m28s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 290h24m29s
CI/CD Pipeline / Frontend Lint (push) Failing after 290h24m29s
CI/CD Pipeline / PR Build API Image (push) Failing after 290h24m32s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 290h24m32s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 290h24m32s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 290h24m33s
CI/CD Pipeline / CI Gate (push) Failing after 290h50m15s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 290h57m11s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 290h57m47s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 290h58m21s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 290h58m50s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 290h58m51s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 290h58m55s
CI/CD Pipeline / PR Build Web Image (push) Failing after 290h58m59s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (push) Successful in 6s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 18s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 16s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 33s
CI/CD Pipeline / Build Staging API Image (push) Successful in 26s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 40s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m54s
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m7s
AI Code Review / AI Code Review (pull_request) Failing after 2m25s
CI/CD Pipeline / Integration Tests (push) Successful in 2m28s
CI/CD Pipeline / Validate - Style (push) Successful in 2m50s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m1s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m1s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m34s
CI/CD Pipeline / Validate - Security (push) Successful in 5m2s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 5m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m8s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 3m56s
CI/CD Pipeline / Unit Tests (push) Successful in 8m40s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 290h15m45s
CI/CD Pipeline / Deploy Production (push) Failing after 290h15m45s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 290h15m48s
CI/CD Pipeline / Build Production Web Image (push) Failing after 290h15m48s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 290h21m26s
CI/CD Pipeline / Build Production API Image (push) Failing after 290h15m48s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 290h22m39s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 290h22m39s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 290h22m39s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 290h22m40s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 290h22m43s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 290h22m43s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 290h22m44s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 290h22m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 290h23m22s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 290h23m23s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 290h23m55s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 290h23m55s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 290h24m23s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 290h24m23s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 290h24m24s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 290h24m24s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 290h24m24s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 290h24m28s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 290h24m29s
CI/CD Pipeline / Frontend Lint (push) Failing after 290h24m29s
CI/CD Pipeline / PR Build API Image (push) Failing after 290h24m32s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 290h24m32s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 290h24m32s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 290h24m33s
CI/CD Pipeline / CI Gate (push) Failing after 290h50m15s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 290h57m11s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 290h57m47s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 290h58m21s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 290h58m50s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 290h58m51s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 290h58m55s
CI/CD Pipeline / PR Build Web Image (push) Failing after 290h58m59s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #1684.
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
"""Tests for two-phase commit pattern in dedup_helpers (#1664 follow-up).
|
||||
|
||||
Verifies that the new dedup_helpers.py:
|
||||
1. Creates video with all dedup fields in a single commit
|
||||
2. Still creates video when fingerprint computation fails
|
||||
3. Creates video with fingerprint but no rate when rate computation fails
|
||||
4. Never does a partial commit (no create + separate update)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# Mock cv2/numpy before imports
|
||||
sys.modules.setdefault("cv2", MagicMock())
|
||||
sys.modules.setdefault("numpy", MagicMock())
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "apps" / "api"))
|
||||
sys.path.insert(0, str(ROOT / "packages"))
|
||||
sys.path.insert(0, str(ROOT / "apps" / "worker"))
|
||||
|
||||
import os
|
||||
|
||||
os.environ.setdefault("JWT_SECRET_KEY", "unit-test-secret")
|
||||
os.environ.setdefault("DATABASE_URL", "sqlite:///test.db")
|
||||
|
||||
import pytest
|
||||
from video_processing.dedup_helpers import create_video_record_and_dedup
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session():
|
||||
s = MagicMock()
|
||||
return s
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_fingerprint():
|
||||
fp = MagicMock()
|
||||
fp.duration = 15000 # 15 seconds in ms
|
||||
fp.to_dict.return_value = {"md5": "abc123", "keyframe_phashes": ["aabb"], "color_histograms": []}
|
||||
fp.chunks = []
|
||||
fp.keyframe_phashes = ["aabb"]
|
||||
fp.color_histograms = []
|
||||
fp.md5 = "abc123"
|
||||
return fp
|
||||
|
||||
|
||||
class TestTwoPhaseCommit:
|
||||
"""Verify that dedup data is computed before commit."""
|
||||
|
||||
def test_video_created_with_all_dedup_fields(self, session, mock_fingerprint):
|
||||
"""When all computations succeed, video is created with all fields in one commit."""
|
||||
mock_repo = MagicMock()
|
||||
mock_deduplicator = MagicMock()
|
||||
mock_deduplicator.compute_fingerprint.return_value = mock_fingerprint
|
||||
mock_deduplicator.check_duplicate.return_value = None
|
||||
mock_deduplicator.compute_duplicate_rate.return_value = {
|
||||
"duplicate_rate": 42.5,
|
||||
"visual_similarity": 0.75,
|
||||
"match_count": 2,
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository",
|
||||
return_value=mock_repo,
|
||||
),
|
||||
patch("video_processing.dedup.VideoDeduplicator", return_value=mock_deduplicator),
|
||||
patch("video_processing.dedup._save_fingerprint_chunks"),
|
||||
):
|
||||
result = create_video_record_and_dedup(
|
||||
generation_task_id="task-001",
|
||||
project_id="proj-001",
|
||||
user_id="user-001",
|
||||
batch_id="",
|
||||
file_url="https://example.com/v.mp4",
|
||||
file_size=1024,
|
||||
duration=15.0,
|
||||
video_path="/tmp/fake.mp4",
|
||||
mode="smart",
|
||||
session=session,
|
||||
)
|
||||
|
||||
assert result == 1
|
||||
# create() should be called exactly once with the complete video object
|
||||
mock_repo.create.assert_called_once()
|
||||
created_video = mock_repo.create.call_args[0][0]
|
||||
assert created_video.duplicate_rate == 42.5
|
||||
assert created_video.visual_similarity == 0.75
|
||||
assert created_video.match_count == 2
|
||||
assert created_video.video_fingerprint is not None
|
||||
# session.commit should be called exactly once (at the end)
|
||||
session.commit.assert_called_once()
|
||||
|
||||
def test_video_created_even_when_fingerprint_fails(self, session):
|
||||
"""When fingerprint computation fails, video is still created (without dedup data)."""
|
||||
mock_repo = MagicMock()
|
||||
mock_deduplicator = MagicMock()
|
||||
mock_deduplicator.compute_fingerprint.side_effect = RuntimeError("cv2 not available")
|
||||
|
||||
with (
|
||||
patch(
|
||||
"packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository",
|
||||
return_value=mock_repo,
|
||||
),
|
||||
patch("video_processing.dedup.VideoDeduplicator", return_value=mock_deduplicator),
|
||||
):
|
||||
result = create_video_record_and_dedup(
|
||||
generation_task_id="task-002",
|
||||
project_id="proj-001",
|
||||
user_id="user-001",
|
||||
batch_id="",
|
||||
file_url="https://example.com/v.mp4",
|
||||
file_size=1024,
|
||||
duration=15.0,
|
||||
video_path="/tmp/fake.mp4",
|
||||
mode="smart",
|
||||
session=session,
|
||||
)
|
||||
|
||||
assert result == 1
|
||||
mock_repo.create.assert_called_once()
|
||||
created_video = mock_repo.create.call_args[0][0]
|
||||
assert created_video.duplicate_rate is None
|
||||
assert created_video.video_fingerprint is None
|
||||
session.commit.assert_called_once()
|
||||
# No dedup methods should have been called
|
||||
mock_deduplicator.check_duplicate.assert_not_called()
|
||||
mock_deduplicator.compute_duplicate_rate.assert_not_called()
|
||||
|
||||
def test_video_created_with_fingerprint_but_no_rate(self, session, mock_fingerprint):
|
||||
"""When rate computation fails, video is created with fingerprint but no rate."""
|
||||
mock_repo = MagicMock()
|
||||
mock_deduplicator = MagicMock()
|
||||
mock_deduplicator.compute_fingerprint.return_value = mock_fingerprint
|
||||
mock_deduplicator.check_duplicate.return_value = None
|
||||
mock_deduplicator.compute_duplicate_rate.side_effect = RuntimeError("DB error")
|
||||
|
||||
with (
|
||||
patch(
|
||||
"packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository",
|
||||
return_value=mock_repo,
|
||||
),
|
||||
patch("video_processing.dedup.VideoDeduplicator", return_value=mock_deduplicator),
|
||||
patch("video_processing.dedup._save_fingerprint_chunks"),
|
||||
):
|
||||
result = create_video_record_and_dedup(
|
||||
generation_task_id="task-003",
|
||||
project_id="proj-001",
|
||||
user_id="user-001",
|
||||
batch_id="",
|
||||
file_url="https://example.com/v.mp4",
|
||||
file_size=1024,
|
||||
duration=15.0,
|
||||
video_path="/tmp/fake.mp4",
|
||||
mode="smart",
|
||||
session=session,
|
||||
)
|
||||
|
||||
assert result == 1
|
||||
mock_repo.create.assert_called_once()
|
||||
created_video = mock_repo.create.call_args[0][0]
|
||||
# Fingerprint should be set
|
||||
assert created_video.video_fingerprint is not None
|
||||
# But duplicate_rate should be None
|
||||
assert created_video.duplicate_rate is None
|
||||
session.commit.assert_called_once()
|
||||
|
||||
def test_no_separate_update_call(self, session, mock_fingerprint):
|
||||
"""Verify the new pattern uses create() only, not create() + update()."""
|
||||
mock_repo = MagicMock()
|
||||
mock_deduplicator = MagicMock()
|
||||
mock_deduplicator.compute_fingerprint.return_value = mock_fingerprint
|
||||
mock_deduplicator.check_duplicate.return_value = None
|
||||
mock_deduplicator.compute_duplicate_rate.return_value = {
|
||||
"duplicate_rate": 10.0,
|
||||
"visual_similarity": 0.5,
|
||||
"match_count": 1,
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository",
|
||||
return_value=mock_repo,
|
||||
),
|
||||
patch("video_processing.dedup.VideoDeduplicator", return_value=mock_deduplicator),
|
||||
patch("video_processing.dedup._save_fingerprint_chunks"),
|
||||
):
|
||||
create_video_record_and_dedup(
|
||||
generation_task_id="task-004",
|
||||
project_id="proj-001",
|
||||
user_id="user-001",
|
||||
batch_id="",
|
||||
file_url="https://example.com/v.mp4",
|
||||
file_size=1024,
|
||||
duration=15.0,
|
||||
video_path="/tmp/fake.mp4",
|
||||
mode="smart",
|
||||
session=session,
|
||||
)
|
||||
|
||||
# Only create() should be called, not update()
|
||||
mock_repo.create.assert_called_once()
|
||||
mock_repo.update.assert_not_called()
|
||||
|
||||
def test_commit_not_called_on_total_failure(self, session):
|
||||
"""When the entire function fails, session.rollback is called instead of commit."""
|
||||
mock_repo = MagicMock()
|
||||
mock_repo.create.side_effect = RuntimeError("DB connection lost")
|
||||
|
||||
with patch(
|
||||
"packages.adapters.sqlalchemy_impl.generated_video_repository.SQLAlchemyGeneratedVideoRepository",
|
||||
return_value=mock_repo,
|
||||
):
|
||||
result = create_video_record_and_dedup(
|
||||
generation_task_id="task-005",
|
||||
project_id="proj-001",
|
||||
user_id="user-001",
|
||||
batch_id="",
|
||||
file_url="https://example.com/v.mp4",
|
||||
file_size=1024,
|
||||
duration=15.0,
|
||||
video_path="/tmp/fake.mp4",
|
||||
mode="smart",
|
||||
session=session,
|
||||
)
|
||||
|
||||
assert result == 0
|
||||
session.commit.assert_not_called()
|
||||
session.rollback.assert_called_once()
|
||||
Reference in New Issue
Block a user