Spaces:
Running
Running
Download backend/tests/test_segment_format_feedback.py from abinazebinoy/verifile-x-api: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/spaces/abinazebinoy/verifile-x-api/resolve/main/backend/tests/test_segment_format_feedback.py
- Command line
-
hf download hf://spaces/abinazebinoy/verifile-x-api/backend/tests/test_segment_format_feedback.py
-
curl -L -o test_segment_format_feedback.py https://huggingface.co/spaces/abinazebinoy/verifile-x-api/resolve/main/backend/tests/test_segment_format_feedback.py
14 kB
| """ | |
| Tests for segment-level AI detection, TIFF/HEIC format support, | |
| and the analyst feedback weight-adaptation system. | |
| """ | |
| import io | |
| import json | |
| import math | |
| import numpy as np | |
| import pytest | |
| from pathlib import Path | |
| from PIL import Image | |
| # ββ Image factories ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _make_jpeg(w=128, h=128): | |
| rng = np.random.default_rng(42) | |
| arr = rng.integers(0, 256, (h, w, 3), dtype=np.uint8) | |
| buf = io.BytesIO() | |
| Image.fromarray(arr, "RGB").save(buf, format="JPEG", quality=85) | |
| return buf.getvalue() | |
| def _make_png(w=128, h=128): | |
| rng = np.random.default_rng(7) | |
| arr = rng.integers(0, 256, (h, w, 3), dtype=np.uint8) | |
| buf = io.BytesIO() | |
| Image.fromarray(arr, "RGB").save(buf, format="PNG") | |
| return buf.getvalue() | |
| def _make_tiff(w=128, h=128): | |
| rng = np.random.default_rng(13) | |
| arr = rng.integers(0, 256, (h, w, 3), dtype=np.uint8) | |
| buf = io.BytesIO() | |
| Image.fromarray(arr, "RGB").save(buf, format="TIFF") | |
| return buf.getvalue() | |
| def _make_tiny(): | |
| arr = np.zeros((16, 16, 3), dtype=np.uint8) | |
| buf = io.BytesIO() | |
| Image.fromarray(arr, "RGB").save(buf, format="JPEG", quality=80) | |
| return buf.getvalue() | |
| def _make_corrupted(): | |
| return b"\xff\xd8\xff\xe0" + b"\x00" * 50 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Phase 27 β Segment-level detection | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestSegmentDetector: | |
| def test_returns_required_keys(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_jpeg(), "test.jpg") | |
| for k in ("grid", "grid_rows", "grid_cols", "tile_size", | |
| "stride", "max_score", "mean_score", "hot_tiles", "coverage"): | |
| assert k in r, f"Missing key: {k}" | |
| def test_grid_is_2d_list(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_jpeg(), "test.jpg") | |
| assert isinstance(r["grid"], list) | |
| assert all(isinstance(row, list) for row in r["grid"]) | |
| def test_grid_dimensions_match(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_jpeg(), "test.jpg") | |
| assert len(r["grid"]) == r["grid_rows"] | |
| if r["grid_rows"] > 0: | |
| assert len(r["grid"][0]) == r["grid_cols"] | |
| def test_grid_values_in_unit_range(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_jpeg(), "test.jpg") | |
| for row in r["grid"]: | |
| for v in row: | |
| assert 0.0 <= v <= 1.0, f"Grid value out of range: {v}" | |
| def test_max_score_in_unit_range(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_jpeg(), "test.jpg") | |
| assert 0.0 <= r["max_score"] <= 1.0 | |
| def test_mean_score_in_unit_range(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_jpeg(), "test.jpg") | |
| assert 0.0 <= r["mean_score"] <= 1.0 | |
| def test_coverage_in_unit_range(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_jpeg(), "test.jpg") | |
| assert 0.0 <= r["coverage"] <= 1.0 | |
| def test_tile_size_and_stride_correct(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_jpeg(), "test.jpg") | |
| assert r["tile_size"] == 64 | |
| assert r["stride"] == 32 | |
| def test_hot_tiles_non_negative(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_jpeg(), "test.jpg") | |
| assert r["hot_tiles"] >= 0 | |
| def test_tiny_image_returns_fallback(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_tiny(), "tiny.jpg") | |
| assert r["grid"] == [] | |
| assert r["grid_rows"] == 0 | |
| def test_corrupted_returns_fallback(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_corrupted(), "bad.jpg") | |
| assert r["grid_rows"] == 0 | |
| assert "error" in r | |
| def test_png_input_handled(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_png(), "test.png") | |
| assert isinstance(r["grid"], list) | |
| def test_no_nan_or_inf(self): | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_jpeg(), "test.jpg") | |
| for k in ("max_score", "mean_score", "coverage"): | |
| assert math.isfinite(r[k]), f"Non-finite in '{k}': {r[k]}" | |
| def test_deterministic(self): | |
| from backend.services.segment_detector import detect_segments | |
| b = _make_jpeg(64, 64) | |
| r1 = detect_segments(b, "det.jpg") | |
| r2 = detect_segments(b, "det.jpg") | |
| assert r1["max_score"] == r2["max_score"] | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Phase 28 β TIFF and HEIC format support | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestFormatSupport: | |
| def test_tiff_in_allowed_types(self): | |
| from backend.core.config import settings | |
| assert "image/tiff" in settings.ALLOWED_IMAGE_TYPES | |
| def test_heic_in_allowed_types(self): | |
| from backend.core.config import settings | |
| assert "image/heic" in settings.ALLOWED_IMAGE_TYPES | |
| def test_heif_in_allowed_types(self): | |
| from backend.core.config import settings | |
| assert "image/heif" in settings.ALLOWED_IMAGE_TYPES | |
| def test_tiff_image_opens_correctly(self): | |
| """PIL must be able to open a TIFF image.""" | |
| tiff_bytes = _make_tiff() | |
| img = Image.open(io.BytesIO(tiff_bytes)) | |
| assert img.format == "TIFF" | |
| def test_pillow_heif_registration_in_main(self): | |
| """main.py must attempt to register pillow_heif.""" | |
| src = (Path(__file__).parent.parent / "main.py").read_text(encoding="utf-8") | |
| assert "pillow_heif" in src | |
| assert "register_heif_opener" in src | |
| def test_pillow_heif_in_requirements(self): | |
| """pillow-heif must appear in requirements.txt.""" | |
| req = (Path(__file__).parent.parent / "requirements.txt").read_text(encoding="utf-8") | |
| assert "pillow-heif" in req | |
| def test_tiff_segment_detection_works(self): | |
| """segment_detector must handle TIFF input without error.""" | |
| from backend.services.segment_detector import detect_segments | |
| r = detect_segments(_make_tiff(), "test.tiff") | |
| assert isinstance(r["grid"], list) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Phase 29 β Analyst feedback weight adaptation | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def tmp_feedback(tmp_path, monkeypatch): | |
| import backend.services.feedback_manager as fm | |
| monkeypatch.setattr(fm, "_DATA_DIR", tmp_path) | |
| monkeypatch.setattr(fm, "_FEEDBACK_PATH", tmp_path / "feedback.jsonl") | |
| monkeypatch.setattr(fm, "_WEIGHTS_PATH", tmp_path / "signal_weights.json") | |
| yield fm | |
| def _sample_signals(): | |
| return [ | |
| {"signal_name": "CLIP", "score": 0.8, "confidence": 0.7}, | |
| {"signal_name": "ELA", "score": 0.75, "confidence": 0.8}, | |
| {"signal_name": "DCT", "score": 0.6, "confidence": 0.6}, | |
| ] | |
| class TestFeedbackManager: | |
| def test_record_returns_required_keys(self, tmp_feedback): | |
| r = tmp_feedback.record_feedback( | |
| evidence_id="eid-001", | |
| true_label="authentic", | |
| predicted_label="ai_generated", | |
| signals=_sample_signals(), | |
| ) | |
| for k in ("feedback_id", "was_wrong", "weights_updated", "total_signals"): | |
| assert k in r | |
| def test_wrong_prediction_updates_weights(self, tmp_feedback): | |
| """Guilty signals must receive a penalty when prediction is wrong.""" | |
| tmp_feedback.record_feedback( | |
| evidence_id="eid-002", | |
| true_label="authentic", | |
| predicted_label="ai_generated", | |
| signals=_sample_signals(), | |
| ) | |
| weights = tmp_feedback.load_weights() | |
| # At least one signal should be penalised | |
| assert any(v < 1.0 for v in weights.values()) | |
| def test_correct_prediction_no_weight_change(self, tmp_feedback): | |
| """Correct predictions must not change any weights.""" | |
| tmp_feedback.record_feedback( | |
| evidence_id="eid-003", | |
| true_label="ai_generated", | |
| predicted_label="ai_generated", | |
| signals=_sample_signals(), | |
| ) | |
| weights = tmp_feedback.load_weights() | |
| assert weights == {} # no updates when prediction was right | |
| def test_invalid_true_label_raises(self, tmp_feedback): | |
| with pytest.raises(ValueError, match="true_label"): | |
| tmp_feedback.record_feedback( | |
| evidence_id="eid-004", | |
| true_label="unknown", | |
| predicted_label="ai_generated", | |
| signals=[], | |
| ) | |
| def test_feedback_logged_to_disk(self, tmp_feedback): | |
| tmp_feedback.record_feedback( | |
| evidence_id="eid-005", | |
| true_label="authentic", | |
| predicted_label="ai_generated", | |
| signals=_sample_signals(), | |
| ) | |
| lines = (tmp_feedback._FEEDBACK_PATH).read_text().splitlines() | |
| assert len(lines) == 1 | |
| rec = json.loads(lines[0]) | |
| assert rec["evidence_id"] == "eid-005" | |
| def test_weight_clipped_to_min(self, tmp_feedback): | |
| """Weights must never drop below MIN_WEIGHT.""" | |
| # Submit many wrong feedbacks | |
| for i in range(30): | |
| tmp_feedback.record_feedback( | |
| evidence_id=f"eid-{i:03d}", | |
| true_label="authentic", | |
| predicted_label="ai_generated", | |
| signals=[{"signal_name": "CLIP", "score": 0.9, "confidence": 0.9}], | |
| ) | |
| weights = tmp_feedback.load_weights() | |
| for v in weights.values(): | |
| assert v >= tmp_feedback._MIN_WEIGHT | |
| def test_get_feedback_history_empty(self, tmp_feedback): | |
| assert tmp_feedback.get_feedback_history() == [] | |
| def test_get_feedback_history_returns_records(self, tmp_feedback): | |
| tmp_feedback.record_feedback( | |
| evidence_id="eid-006", | |
| true_label="authentic", | |
| predicted_label="ai_generated", | |
| signals=_sample_signals(), | |
| ) | |
| history = tmp_feedback.get_feedback_history() | |
| assert len(history) == 1 | |
| assert history[0]["evidence_id"] == "eid-006" | |
| def test_get_feedback_history_filter_by_id(self, tmp_feedback): | |
| for eid in ["eid-007", "eid-008", "eid-009"]: | |
| tmp_feedback.record_feedback( | |
| evidence_id=eid, | |
| true_label="authentic", | |
| predicted_label="ai_generated", | |
| signals=_sample_signals(), | |
| ) | |
| filtered = tmp_feedback.get_feedback_history(evidence_id="eid-008") | |
| assert len(filtered) == 1 | |
| assert filtered[0]["evidence_id"] == "eid-008" | |
| def test_get_weight_summary_keys(self, tmp_feedback): | |
| summary = tmp_feedback.get_weight_summary() | |
| for k in ("signal_weights", "total_overrides", | |
| "signals_penalised", "signals_boosted"): | |
| assert k in summary | |
| def test_feedback_id_is_unique(self, tmp_feedback): | |
| import time | |
| r1 = tmp_feedback.record_feedback( | |
| "eid-010", "authentic", "ai_generated", _sample_signals() | |
| ) | |
| time.sleep(0.01) | |
| r2 = tmp_feedback.record_feedback( | |
| "eid-011", "authentic", "ai_generated", _sample_signals() | |
| ) | |
| assert r1["feedback_id"] != r2["feedback_id"] | |
| class TestFeedbackAPIAuth: | |
| def client(self): | |
| from fastapi import FastAPI | |
| from fastapi.testclient import TestClient | |
| from backend.api.routes.feedback import router | |
| app = FastAPI() | |
| app.include_router(router) | |
| return TestClient(app, raise_server_exceptions=False) | |
| def test_submit_requires_auth(self, client): | |
| resp = client.post( | |
| "/api/v1/feedback/", | |
| json={ | |
| "evidence_id": "x", "true_label": "authentic", | |
| "predicted_label": "ai_generated", "signals": [], | |
| }, | |
| ) | |
| assert resp.status_code == 401 | |
| def test_list_requires_auth(self, client): | |
| assert client.get("/api/v1/feedback/").status_code == 401 | |
| def test_weights_requires_auth(self, client): | |
| assert client.get("/api/v1/feedback/weights").status_code == 401 | |