"""TextDetectionPipeline: the inverted gate. Fake detectors are injected so no model is ever loaded. """ import text_detection.pipeline as tp from text_detection.schema import DetectionResult, Verdict class _FakeDetector: def __init__(self, name, p_fake, verdict, confidence, error=None): self.name = name self._r = DetectionResult( detector=name, p_fake=p_fake, verdict=verdict, confidence=confidence, error=error, ) async def detect_text(self, text): return self._r class _RecordingVendor: """Stands in for the paid fallback; records whether it was consulted.""" name = "text_vendor" def __init__(self, p_fake=0.88, verdict=Verdict.AI_GENERATED, confidence=0.76): self.called = False self._r = DetectionResult( detector="text_vendor", p_fake=p_fake, verdict=verdict, confidence=confidence, ) async def detect_text(self, text): self.called = True return self._r LONG_TEXT = " ".join(["word"] * 80) async def test_confident_local_result_skips_vendor(monkeypatch): # Two AI voters, one of them non-zero-shot -> firewall keeps AI, no vendor call. local = [ _FakeDetector("text_watermark", 0.98, Verdict.AI_GENERATED, 0.95), _FakeDetector("text_fastdetectgpt", 0.90, Verdict.AI_GENERATED, 0.80), ] monkeypatch.setattr(tp.TextDetectionPipeline, "_primary_detectors", lambda self: local) vendor = _RecordingVendor() res = await tp.TextDetectionPipeline(vendor_detector=vendor).run(LONG_TEXT) assert vendor.called is False assert res.verdict == Verdict.AI_GENERATED async def test_unconfident_local_triggers_vendor(monkeypatch): local = [_FakeDetector("text_fastdetectgpt", 0.52, Verdict.UNCERTAIN, 0.04)] monkeypatch.setattr(tp.TextDetectionPipeline, "_primary_detectors", lambda self: local) vendor = _RecordingVendor() await tp.TextDetectionPipeline(vendor_detector=vendor).run(LONG_TEXT) assert vendor.called is True async def test_no_local_result_falls_back_to_vendor(monkeypatch): # Every local detector errored -> never fail closed -> consult the vendor. local = [_FakeDetector("text_fastdetectgpt", 0.5, Verdict.UNCERTAIN, 0.0, error="boom")] monkeypatch.setattr(tp.TextDetectionPipeline, "_primary_detectors", lambda self: local) vendor = _RecordingVendor(p_fake=0.20, verdict=Verdict.REAL, confidence=0.60) res = await tp.TextDetectionPipeline(vendor_detector=vendor).run(LONG_TEXT) assert vendor.called is True assert any(r.detector == "text_vendor" for r in res.detector_results) async def test_no_vendor_configured_degrades_instead_of_raising(monkeypatch): local = [_FakeDetector("text_fastdetectgpt", 0.5, Verdict.UNCERTAIN, 0.0, error="boom")] monkeypatch.setattr(tp.TextDetectionPipeline, "_primary_detectors", lambda self: local) res = await tp.TextDetectionPipeline().run(LONG_TEXT) assert res.verdict == Verdict.UNCERTAIN assert res.confidence == 0.0 async def test_conflict_triggers_vendor(monkeypatch): local = [ _FakeDetector("text_watermark", 0.95, Verdict.AI_GENERATED, 0.90), _FakeDetector("text_fastdetectgpt", 0.05, Verdict.REAL, 0.90), ] monkeypatch.setattr(tp.TextDetectionPipeline, "_primary_detectors", lambda self: local) vendor = _RecordingVendor() await tp.TextDetectionPipeline(vendor_detector=vendor).run(LONG_TEXT) assert vendor.called is True async def test_short_text_is_capped_by_firewall(monkeypatch): local = [_FakeDetector("text_watermark", 0.98, Verdict.AI_GENERATED, 0.95)] monkeypatch.setattr(tp.TextDetectionPipeline, "_primary_detectors", lambda self: local) res = await tp.TextDetectionPipeline().run("short and machine-like but only nine words") assert res.verdict == Verdict.UNCERTAIN assert res.confidence <= 0.30 async def test_empty_text_returns_uncertain_without_calls(): vendor = _RecordingVendor() res = await tp.TextDetectionPipeline(vendor_detector=vendor).run(" ") assert res.verdict == Verdict.UNCERTAIN assert res.confidence == 0.0 assert vendor.called is False async def test_no_detectors_enabled_returns_uncertain(): # Default config: everything disabled. Must abstain, not guess. res = await tp.TextDetectionPipeline().run(LONG_TEXT) assert res.verdict == Verdict.UNCERTAIN assert res.confidence == 0.0