Files
photoanalyzer/tests/unit/test_safety.py

69 lines
2.4 KiB
Python

"""Extracted safety logic: scoring bands, keyword rules, and no CLI dependency."""
import ast
from pathlib import Path
import pytest
from photo_pipeline.services import safety
REPO = Path(__file__).resolve().parents[2]
@pytest.mark.parametrize(
"score,expected",
[(0.95, "nsfw"), (0.6, "nsfw"), (0.59, "review_required"), (0.2, "review_required"), (0.05, "sfw")],
)
def test_classify_bands(score, expected):
assert safety.classify(score) == expected
def test_normalize_keywords_handles_list_scalar_and_none():
assert safety.normalize_keywords(["NSFW", " Sfw "]) == {"nsfw", "sfw"}
assert safety.normalize_keywords("NSFW") == {"nsfw"}
assert safety.normalize_keywords(None) == set()
def test_has_nsfw():
assert safety.has_nsfw(["Foo", "NSFW"])
assert not safety.has_nsfw(["sfw", "beach"])
def test_marks_nsfw_wins_over_sfw():
marks = safety.marks_from_keywords(
{"a.jpg": ["nsfw", "sfw"], "b.jpg": ["sfw"], "c.jpg": ["landscape"]}
)
assert marks == {"nsfw": {"a.jpg"}, "sfw": {"b.jpg"}}
def test_partition_nsfw_gates_analysis():
analyzable, skipped = safety.partition_nsfw({"keep.jpg": ["x"], "block.jpg": ["nsfw"]})
assert analyzable == ["keep.jpg"]
assert skipped == ["block.jpg"]
def test_exif_projection_is_mutually_exclusive():
assert safety.exif_projection("nsfw") == {"add": ["nsfw"], "remove": ["sfw"]}
assert safety.exif_projection("sfw") == {"add": ["sfw"], "remove": ["nsfw"]}
assert safety.exif_projection("review_required") == {"add": [], "remove": []}
def test_shared_modules_do_not_import_archived_clis():
"""Acceptance: shared modules have no dependency on archived entry points."""
banned = {"nsfwtag", "photo_analyzer", "nsfw_tag"}
modules = [
REPO / "photo_pipeline" / "services" / "safety.py",
REPO / "photo_pipeline" / "integrations" / "exiftool.py",
REPO / "photo_pipeline" / "integrations" / "nsfw_model.py",
]
for module in modules:
tree = ast.parse(module.read_text(), filename=str(module))
for node in ast.walk(tree):
if isinstance(node, ast.Import):
names = {a.name.split(".")[0] for a in node.names}
elif isinstance(node, ast.ImportFrom):
names = {(node.module or "").split(".")[0]}
else:
continue
assert not (names & banned), f"{module.name} imports archived CLI: {names & banned}"