"""Deep scan re-queues already-completed ImportTasks. Operator-flagged 2026-05-25: deep scan used to skip everything that already had a non-failed ImportTask row, making a deep re-scan a no-op when no new files were added. That defeated the entire point of deep scan (re-apply sidecar metadata to existing rows). The skip-set now splits by mode — quick keeps the old "any non-failed" semantics; deep skips ONLY actively-in-flight statuses. """ import pytest from PIL import Image from sqlalchemy import func, select from backend.app.models import ImportBatch, ImportSettings, ImportTask from backend.app.tasks.scan import scan_directory pytestmark = pytest.mark.integration def _img(path): path.parent.mkdir(parents=True, exist_ok=True) Image.new("RGB", (40, 40), (10, 200, 80)).save(path, "JPEG") def test_deep_scan_requeues_completed_task(db_sync, tmp_path, monkeypatch): """Quick scan then deep scan of the same /import: the file completed in the first run should be re-enqueued by the deep run (different ImportTask id, same source_path).""" import_root = tmp_path / "import" settings = db_sync.execute( select(ImportSettings).where(ImportSettings.id == 1) ).scalar_one() settings.import_scan_path = str(import_root) db_sync.commit() src = import_root / "Mae" / "p.jpg" _img(src) from backend.app import celery_app celery_app.celery.conf.task_always_eager = False # explicit try: first_batch_id = scan_directory.run(triggered_by="manual", mode="quick") first_task = db_sync.execute( select(ImportTask).where(ImportTask.batch_id == first_batch_id) ).scalar_one() # Simulate the worker having finished it. first_task.status = "complete" db_sync.commit() # Now deep scan: the SAME file should get a NEW ImportTask row. second_batch_id = scan_directory.run(triggered_by="manual", mode="deep") assert second_batch_id != first_batch_id new_tasks_in_second_batch = db_sync.execute( select(func.count()) .select_from(ImportTask) .where(ImportTask.batch_id == second_batch_id) ).scalar_one() assert new_tasks_in_second_batch == 1, ( "deep scan did not re-queue the completed file" ) # And the second batch's task should be for the same source_path # as the first (proves it's a re-queue, not a different file). sp = db_sync.execute( select(ImportTask.source_path) .where(ImportTask.batch_id == second_batch_id) ).scalar_one() assert sp == str(src) finally: celery_app.celery.conf.task_always_eager = False def test_quick_scan_does_not_requeue_completed_task(db_sync, tmp_path): """The flip side: quick scan still keeps the old skip semantics — a file with a completed ImportTask row from a prior batch is NOT re-enqueued on a fresh quick scan.""" import_root = tmp_path / "import" settings = db_sync.execute( select(ImportSettings).where(ImportSettings.id == 1) ).scalar_one() settings.import_scan_path = str(import_root) db_sync.commit() src = import_root / "Mae" / "q.jpg" _img(src) first_batch_id = scan_directory.run(triggered_by="manual", mode="quick") first_task = db_sync.execute( select(ImportTask).where(ImportTask.batch_id == first_batch_id) ).scalar_one() first_task.status = "complete" db_sync.commit() second_batch_id = scan_directory.run(triggered_by="manual", mode="quick") second_batch_count = db_sync.execute( select(func.count()) .select_from(ImportTask) .where(ImportTask.batch_id == second_batch_id) ).scalar_one() assert second_batch_count == 0, ( "quick scan re-queued an already-completed task — should have skipped" ) # And the second batch should self-finalize (files_seen=0). second_batch = db_sync.get(ImportBatch, second_batch_id) assert second_batch.status == "complete"