import pytest from backend.app.models import ImageRecord, TagKind from backend.app.models.tag import image_tag from backend.app.services.ml.aliases import AliasService from backend.app.services.ml.suggestions import SuggestionService from backend.app.services.tag_service import TagService pytestmark = pytest.mark.integration def _img(sha: str) -> ImageRecord: return ImageRecord( path=f"/images/{sha}.jpg", sha256=sha, size_bytes=1, mime="image/jpeg", width=1, height=1, origin="imported_filesystem", integrity_status="unknown", ) async def _seed_img(db, sha: str, predictions: dict) -> ImageRecord: """#768: create an image + seed its predictions into image_prediction (the read path's source), returning the flushed record.""" from tests._prediction_helpers import seed_predictions img = _img(sha) db.add(img) await db.flush() await seed_predictions(db, img.id, predictions) return img @pytest.mark.asyncio async def test_threshold_filters_low_confidence_general(db): # Default general threshold is 0.50 (alembic 0029 lowered it from # 0.95). Use 0.30/0.60 to keep the test asserting threshold behavior # rather than the exact cutoff number. img = await _seed_img( db, "a" * 64, { "lowconf": {"category": "general", "confidence": 0.30}, "sword": {"category": "general", "confidence": 0.97}, }, ) sl = await SuggestionService(db).for_image(img.id) names = [s.display_name for s in sl.by_category.get("general", [])] # display_name is normalized (tag_name.normalize) before surfacing. assert "Sword" in names assert "Lowconf" not in names @pytest.mark.asyncio async def test_threshold_override_surfaces_low_confidence(db): # The typed-dropdown "show everything the model saw" mode: threshold_override # surfaces stored predictions below the configured threshold (in canonical # formatting) so they can be picked instead of hand-typed (2026-06-09). img = await _seed_img( db, "d" * 64, { "lowconf": {"category": "general", "confidence": 0.30}, "sword": {"category": "general", "confidence": 0.97}, }, ) sl = await SuggestionService(db).for_image(img.id, threshold_override=0.0) names = [s.display_name for s in sl.by_category.get("general", [])] assert "Sword" in names assert "Lowconf" in names # below the configured threshold, surfaced anyway # Unsurfaced categories are still excluded even with the override. img2 = await _seed_img( db, "e" * 64, {"safe": {"category": "rating", "confidence": 0.99}} ) sl2 = await SuggestionService(db).for_image(img2.id, threshold_override=0.0) assert "rating" not in sl2.by_category @pytest.mark.asyncio async def test_unsurfaced_category_dropped(db): img = await _seed_img( db, "b" * 64, {"safe": {"category": "rating", "confidence": 0.99}}, ) sl = await SuggestionService(db).for_image(img.id) assert "rating" not in sl.by_category @pytest.mark.asyncio async def test_alias_resolution(db): tags = TagService(db) canonical = await tags.find_or_create("Sasuke Uchiha", TagKind.character) await AliasService(db).create("uchiha_sasuke", "character", canonical.id) img = await _seed_img( db, "c" * 64, {"uchiha_sasuke": {"category": "character", "confidence": 0.96}}, ) sl = await SuggestionService(db).for_image(img.id) chars = sl.by_category["character"] assert len(chars) == 1 assert chars[0].display_name == "Sasuke Uchiha" assert chars[0].canonical_tag_id == canonical.id assert chars[0].creates_new_tag is False # Surfaced via an alias on the raw model key — the UI marks it + offers undo. assert chars[0].via_alias is True assert chars[0].raw_name == "uchiha_sasuke" @pytest.mark.asyncio async def test_raw_tag_creates_new(db): img = await _seed_img( db, "d" * 64, {"brand_new_tag": {"category": "character", "confidence": 0.96}}, ) sl = await SuggestionService(db).for_image(img.id) chars = sl.by_category["character"] # display_name is the normalized Camie name (underscores -> spaces, # title-cased), not the raw vocab key. assert chars[0].display_name == "Brand New Tag" assert chars[0].creates_new_tag is True # Not aliased, but the raw key is carried so the modal can author one. assert chars[0].via_alias is False assert chars[0].raw_name == "brand_new_tag" assert chars[0].canonical_tag_id is None @pytest.mark.asyncio async def test_applied_tag_not_suggested(db): tags = TagService(db) tag = await tags.find_or_create("alreadyhere", TagKind.character) img = await _seed_img( db, "e" * 64, {"alreadyhere": {"category": "character", "confidence": 0.96}}, ) await db.execute( image_tag.insert().values( image_record_id=img.id, tag_id=tag.id, source="manual" ) ) sl = await SuggestionService(db).for_image(img.id) assert "character" not in sl.by_category or not sl.by_category["character"]