commit remaining modified project files

This commit is contained in:
windyboy
2026-04-07 08:07:18 +08:00
parent d010cf936c
commit fb128c70d6
28 changed files with 4140 additions and 19643 deletions
+109
View File
@@ -232,3 +232,112 @@ def test_review_plan_show_all_overrides_preview_limit(tmp_path):
assert " - [2] " in result.output
assert " - [3] " in result.output
assert "more high-risk operations" not in result.output
def test_review_plan_tui_requires_interactive_terminal(tmp_path):
"""--tui should fail when stdin/stdout are not TTY (e.g. CliRunner)."""
config_path = tmp_path / "config.yaml"
_write_config(config_path, tmp_path / "library")
plan_path = tmp_path / "plan.json"
operations = [
{
"operation_type": "move",
"source_path": str(tmp_path / "a.mkv"),
"destination_path": str(tmp_path / "b.mkv"),
"reason": "organize",
"has_conflict": False,
"conflict_reason": None,
},
]
_write_plan(
plan_path,
operations=operations,
summary={"total": 1, "move": 1, "rename": 0, "quarantine": 0, "no-op": 0},
)
output_csv = tmp_path / "review.csv"
runner = CliRunner()
result = _invoke_review_plan(
runner,
config_path,
["--input", str(plan_path), "--output", str(output_csv), "--tui"],
)
assert result.exit_code == 1
assert "TTY" in result.output or "interactive terminal" in result.output
def test_review_plan_tui_stubbed_success(tmp_path, monkeypatch):
"""With TTY check stubbed and run_plan_review_tui stubbed, CLI should succeed."""
config_path = tmp_path / "config.yaml"
_write_config(config_path, tmp_path / "library")
plan_path = tmp_path / "plan.json"
operations = [
{
"operation_type": "move",
"source_path": str(tmp_path / "a.mkv"),
"destination_path": str(tmp_path / "b.mkv"),
"reason": "organize",
"has_conflict": False,
"conflict_reason": None,
},
]
_write_plan(
plan_path,
operations=operations,
summary={"total": 1, "move": 1, "rename": 0, "quarantine": 0, "no-op": 0},
)
monkeypatch.setattr("vlm.cli._review_plan_tui_streams_ok", lambda: True)
monkeypatch.setattr("vlm.review_tui.run_plan_review_tui", lambda ctx: 0)
output_csv = tmp_path / "review.csv"
runner = CliRunner()
result = _invoke_review_plan(
runner,
config_path,
["--input", str(plan_path), "--output", str(output_csv), "--tui"],
)
assert result.exit_code == 0
assert "Saved manual review CSV" in result.output
assert "Plan overview:" not in result.output
def test_review_plan_tui_stubbed_abort(tmp_path, monkeypatch):
"""TUI return code 1 should surface as CLI failure."""
config_path = tmp_path / "config.yaml"
_write_config(config_path, tmp_path / "library")
plan_path = tmp_path / "plan.json"
operations = [
{
"operation_type": "move",
"source_path": str(tmp_path / "a.mkv"),
"destination_path": str(tmp_path / "b.mkv"),
"reason": "organize",
"has_conflict": False,
"conflict_reason": None,
},
]
_write_plan(
plan_path,
operations=operations,
summary={"total": 1, "move": 1, "rename": 0, "quarantine": 0, "no-op": 0},
)
monkeypatch.setattr("vlm.cli._review_plan_tui_streams_ok", lambda: True)
monkeypatch.setattr("vlm.review_tui.run_plan_review_tui", lambda ctx: 1)
output_csv = tmp_path / "review.csv"
runner = CliRunner()
result = _invoke_review_plan(
runner,
config_path,
["--input", str(plan_path), "--output", str(output_csv), "--tui"],
)
assert result.exit_code == 1
assert "aborted" in result.output.lower()
+6 -6
View File
@@ -72,10 +72,10 @@ class TestDuplicateQualityWithMetadata:
loaded = load_identities_json(identities_file)
# Convert to analysis input
movie_identities, series_identities, video_files = identities_to_analysis_input(loaded)
movie_pairs, series_pairs = identities_to_analysis_input(loaded)
# Create identity-file pairs
identity_file_pairs = list(zip(movie_identities, video_files))
identity_file_pairs = movie_pairs
# Detect duplicates
duplicates = detect_duplicates(identity_file_pairs)
@@ -151,10 +151,10 @@ class TestDuplicateQualityWithMetadata:
loaded = load_identities_json(identities_file)
# Convert to analysis input
movie_identities, series_identities, video_files = identities_to_analysis_input(loaded)
movie_pairs, series_pairs = identities_to_analysis_input(loaded)
# Create identity-file pairs
identity_file_pairs = list(zip(movie_identities, video_files))
identity_file_pairs = movie_pairs
# Detect duplicates
duplicates = detect_duplicates(identity_file_pairs)
@@ -231,10 +231,10 @@ class TestDuplicateQualityWithMetadata:
json.dump(data, f, indent=2)
loaded = load_identities_json(identities_file)
movie_identities, series_identities, video_files = identities_to_analysis_input(loaded)
movie_pairs, series_pairs = identities_to_analysis_input(loaded)
# Create identity-file pairs for series
identity_file_pairs = list(zip(series_identities, video_files))
identity_file_pairs = series_pairs
# Detect duplicates
duplicates = detect_duplicates(identity_file_pairs)
+70
View File
@@ -8,6 +8,7 @@ import pytest
from vlm.io import (
_video_file_from_record,
load_analysis_json,
load_identities_json,
save_identities_json,
)
@@ -260,3 +261,72 @@ class TestIdentitiesJsonRoundTrip:
assert series["video_metadata"]["size_bytes"] == 500000000
assert series["video_metadata"]["resolution"] == "1920x1080"
assert series["video_metadata"]["codec"] == "h264"
class TestJsonSchemaValidation:
"""Tests for runtime schema validation on JSON artifacts."""
def test_save_identities_rejects_invalid_movie_shape(self, tmp_path):
identities_file = tmp_path / "identities.json"
invalid_data = {
"vlm_schema_version": "2.0",
"metadata": {
"generated": "2024-02-13T12:00:00",
"source_inventory": "test_inventory.csv",
"total_files": 1,
},
"movies": [
{
"path": "/library/movie/Movie1 (2024).mkv",
"filename": "Movie1 (2024).mkv",
"category": "movie",
"title": "Movie1",
"year": 2024,
"confidence": "high",
"needs_review": False,
}
],
"series": [],
"anime": [],
"other": [],
}
with pytest.raises(ValueError, match="confidence"):
save_identities_json(invalid_data, identities_file)
def test_load_analysis_rejects_missing_required_fields(self, tmp_path):
analysis_file = tmp_path / "analysis.json"
analysis_file.write_text(
json.dumps(
{
"vlm_schema_version": "1.0",
"metadata": {"generated": "2024-01-01T00:00:00"},
"completeness": [
{
"series_title": "Show",
"season": 1,
"episodes_found": [1],
"episodes_missing": [2],
}
],
"duplicates": [
{
"identity": {
"type": "movie",
"title": "Test",
"year": 2024,
"season": None,
"episodes": [],
},
"files": ["/library/movie/Test (2024).mkv"],
"quality_comparison": [],
}
],
}
),
encoding="utf-8",
)
loaded = load_analysis_json(analysis_file)
assert loaded["metadata"]["generated"] == "2024-01-01T00:00:00"
assert loaded["duplicates"][0]["identity"]["title"] == "Test"
+65 -1
View File
@@ -1,5 +1,6 @@
"""Unit tests for plan generator."""
import json
import pytest
from datetime import datetime, timezone
from pathlib import Path
@@ -12,7 +13,7 @@ from vlm.models import (
SeriesIdentity,
VideoFile,
)
from vlm.planner import generate_plan
from vlm.planner import generate_plan, load_plan
@pytest.fixture
@@ -1255,3 +1256,66 @@ def test_series_rejected_by_review_generates_noop(config):
plan = generate_plan([(video_file, identity)], config)
assert plan.operations[0].operation_type == "no-op"
assert "rejected" in plan.operations[0].reason.lower()
def test_save_and_load_plan_validates_schema(tmp_path):
"""Saved plan artifacts should round-trip through schema validation."""
plan = ExecutionPlan(
plan_id="plan-123",
created_at=datetime(2026, 4, 2, 12, 0, tzinfo=timezone.utc),
operations=[
FileOperation(
operation_type="move",
source_path=Path("/mnt/nas/videos/movie/source.mkv"),
destination_path=Path("/mnt/nas/videos/movie/target.mkv"),
reason="move movie",
has_conflict=False,
)
],
summary={"move": 1, "rename": 0, "noop": 0, "delete": 0},
summary_by_reason={"move movie": 1},
human_summary="计划已生成",
metadata={"analysis_source": "analysis.json"},
)
output_path = tmp_path / "plan.json"
from vlm.planner import save_plan
save_plan(plan, output_path)
loaded = load_plan(output_path)
assert loaded.plan_id == plan.plan_id
assert loaded.operations[0].source_path == plan.operations[0].source_path
assert loaded.operations[0].destination_path == plan.operations[0].destination_path
assert loaded.summary == plan.summary
assert loaded.summary_by_reason == plan.summary_by_reason
assert loaded.human_summary == plan.human_summary
assert loaded.metadata == plan.metadata
def test_load_plan_rejects_invalid_schema(tmp_path):
"""Invalid plan artifacts should fail validation before deserialization."""
invalid_path = tmp_path / "invalid-plan.json"
invalid_path.write_text(
json.dumps(
{
"vlm_schema_version": "1.0",
"plan_id": "plan-123",
"created_at": "2026-04-02T12:00:00+00:00",
"operations": [
{
"operation_type": "move",
"source_path": "/mnt/nas/videos/movie/source.mkv",
"destination_path": "/mnt/nas/videos/movie/target.mkv",
"has_conflict": False,
}
],
"summary": {"move": 1},
}
),
encoding="utf-8",
)
with pytest.raises(ValueError, match="plan JSON.operations\\[0\\]\\.reason"):
load_plan(invalid_path)
+56
View File
@@ -0,0 +1,56 @@
"""Tests for vlm.review_display helpers."""
from pathlib import Path
from vlm.review_display import (
build_csv_rows,
format_paths_for_detail,
review_row_status_symbol,
risk_flags_to_labels,
)
def test_risk_flags_to_labels():
assert "需人工判断" in risk_flags_to_labels("manual_review")
assert "目标冲突" in risk_flags_to_labels("conflict")
assert risk_flags_to_labels("") == ""
assert risk_flags_to_labels("unknown_flag") == "unknown_flag"
def test_format_paths_for_detail_relative():
root = Path("/media/lib")
text = format_paths_for_detail(
str(root / "a" / "f.mkv"),
str(root / "b" / "f.mkv"),
root,
)
assert "来源:" in text
assert "f.mkv" in text
assert "目标:" in text
def test_build_csv_rows_merges_operation_type():
rows = [
{
"index": "1",
"operation_type": "move",
"risk_flags": "manual_review",
"source_path": "/x",
"destination_path": "/y",
"reason": "r",
}
]
merged = build_csv_rows(rows, {1: "no-op"})
assert merged[0]["operation_type"] == "no-op"
assert merged[0]["risk_flags"] == "manual_review"
def test_review_row_status_symbol():
initial = {1: "move"}
current = {1: "move"}
assert review_row_status_symbol(current, initial, 1) == "·"
current2 = {1: "no-op"}
assert review_row_status_symbol(current2, initial, 1) == ""
current3 = {1: "move"}
initial3 = {1: "no-op"}
assert review_row_status_symbol(current3, initial3, 1) == ""