import pytest from cf_voice.acoustic import ( AcousticBackend, AcousticResult, ASTAcousticBackend, MockAcousticBackend, make_acoustic, ) from cf_voice.events import AudioEvent class TestAcousticResult: def test_fields(self): evt = AudioEvent(timestamp=1.0, event_type="queue", label="ringback", confidence=0.9) result = AcousticResult(queue=evt, speaker=None, environ=None, scene=None, timestamp=1.0) assert result.queue.label == "ringback" assert result.speaker is None assert result.environ is None assert result.scene is None class TestMockAcousticBackend: def test_classify_returns_result(self): backend = MockAcousticBackend(seed=0) result = backend.classify_window(b"", timestamp=0.0) assert isinstance(result, AcousticResult) assert result.timestamp == 0.0 def test_all_events_present(self): backend = MockAcousticBackend(seed=1) result = backend.classify_window(b"", timestamp=1.0) assert result.queue is not None assert result.speaker is not None assert result.environ is not None assert result.scene is not None def test_event_types_correct(self): backend = MockAcousticBackend(seed=2) result = backend.classify_window(b"", timestamp=2.0) assert result.queue.event_type == "queue" assert result.speaker.event_type == "speaker" assert result.environ.event_type == "environ" assert result.scene.event_type == "scene" def test_confidence_in_range(self): backend = MockAcousticBackend(seed=3) for _ in range(5): result = backend.classify_window(b"", timestamp=0.0) assert 0.0 <= result.queue.confidence <= 1.0 assert 0.0 <= result.speaker.confidence <= 1.0 assert 0.0 <= result.environ.confidence <= 1.0 assert 0.0 <= result.scene.confidence <= 1.0 def test_lifecycle_advances(self): """Phases should change after their duration elapses.""" import time backend = MockAcousticBackend(seed=42) # Force phase to advance by manipulating phase_start backend._phase_start -= 1000 # pretend 1000s elapsed result = backend.classify_window(b"", timestamp=0.0) # Should have advanced — just verify it doesn't crash and returns valid assert result.queue.label in ( "hold_music", "silence", "ringback", "busy", "dead_air", "dtmf_tone" ) def test_isinstance_protocol(self): backend = MockAcousticBackend() assert isinstance(backend, AcousticBackend) def test_deterministic_with_seed(self): b1 = MockAcousticBackend(seed=99) b2 = MockAcousticBackend(seed=99) r1 = b1.classify_window(b"", timestamp=0.0) r2 = b2.classify_window(b"", timestamp=0.0) assert r1.queue.label == r2.queue.label assert r1.queue.confidence == r2.queue.confidence class TestASTAcousticBackend: def test_raises_import_error_without_deps(self, monkeypatch): """ASTAcousticBackend should raise ImportError when transformers is unavailable.""" import builtins real_import = builtins.__import__ def mock_import(name, *args, **kwargs): if name in ("transformers",): raise ImportError(f"Mocked: {name} not available") return real_import(name, *args, **kwargs) monkeypatch.setattr(builtins, "__import__", mock_import) with pytest.raises(ImportError, match="transformers"): ASTAcousticBackend() class TestMakeAcoustic: def test_mock_flag(self): backend = make_acoustic(mock=True) assert isinstance(backend, MockAcousticBackend) def test_mock_env(self, monkeypatch): monkeypatch.setenv("CF_VOICE_MOCK", "1") backend = make_acoustic() assert isinstance(backend, MockAcousticBackend) def test_real_falls_back_to_mock_without_deps(self, monkeypatch, capsys): """make_acoustic(mock=False) falls back to mock when deps are missing.""" import builtins real_import = builtins.__import__ def mock_import(name, *args, **kwargs): if name in ("transformers",): raise ImportError(f"Mocked: {name} not available") return real_import(name, *args, **kwargs) monkeypatch.delenv("CF_VOICE_MOCK", raising=False) monkeypatch.setattr(builtins, "__import__", mock_import) backend = make_acoustic(mock=False) # Should fall back gracefully, never raise assert isinstance(backend, MockAcousticBackend)