"""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}"