69 lines
2.4 KiB
Python
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}"
|