"""Tests for cht.stream.manager — StreamManager.""" import json from unittest.mock import patch, MagicMock import pytest from cht.stream.manager import StreamManager @pytest.fixture def manager(tmp_path): with patch("cht.stream.manager.SESSIONS_DIR", tmp_path): mgr = StreamManager(session_id="test_session") yield mgr mgr.stop_all() class TestInit: def test_session_id_custom(self, manager): assert manager.session_id == "test_session" def test_recording_path_delegates_to_recorder(self, manager): assert manager.recording_path.name == "recording_000.mp4" def test_dirs_not_created_on_init(self, manager): assert not manager.stream_dir.exists() def test_relay_url_from_recorder(self, manager): assert "4445" in manager.relay_url class TestSetupDirs: def test_creates_all_subdirs(self, manager): manager.setup_dirs() assert manager.stream_dir.is_dir() assert manager.frames_dir.is_dir() assert manager.transcript_dir.is_dir() assert manager.audio_dir.is_dir() assert manager.agent_dir.is_dir() class TestStopAll: def test_stop_all_calls_processor_and_recorder(self, manager): manager.processor.stop = MagicMock() manager.recorder.stop = MagicMock() manager.stop_all() manager.processor.stop.assert_called_once() manager.recorder.stop.assert_called_once() class TestSceneDetector: def test_python_path_sets_processor_callback(self, manager): """Python path (recorder present): on_new_frames goes to processor, not recorder.""" cb = MagicMock() manager.start_scene_detector(on_new_frames=cb) assert manager.processor._on_new_frames is cb def test_python_path_does_not_start_processor_scene_detector(self, manager): """Python path: recorder owns scene detection — processor.start_scene_detector not called.""" manager.processor.start_scene_detector = MagicMock() manager.start_scene_detector(on_new_frames=MagicMock()) manager.processor.start_scene_detector.assert_not_called() def test_rust_path_sets_callback_and_starts_detector(self, tmp_path): """Rust path (no recorder): processor owns scene detection.""" with patch("cht.stream.manager.SESSIONS_DIR", tmp_path): mgr = StreamManager.__new__(StreamManager) mgr.recorder = None mgr.processor = MagicMock() from cht.stream.manager import SCENE_THRESHOLD cb = MagicMock() mgr.start_scene_detector(on_new_frames=cb) mgr.processor.set_on_new_frames.assert_called_once_with(cb) mgr.processor.start_scene_detector.assert_called_once_with(threshold=SCENE_THRESHOLD) def test_update_scene_threshold_restarts_recorder(self, manager): manager.recorder.update_scene_threshold = MagicMock() manager.update_scene_threshold(0.25) manager.recorder.update_scene_threshold.assert_called_once_with(0.25) class TestFromRustSession: def test_attaches_without_recorder(self, tmp_path): session_dir = tmp_path / "20260410_120000" session_dir.mkdir() (session_dir / "stream").mkdir() mgr = StreamManager.from_rust_session(session_dir) assert mgr.recorder is None assert mgr.session_id == "20260410_120000" def test_relay_url_fallback_without_recorder(self, tmp_path): session_dir = tmp_path / "20260410_120000" session_dir.mkdir() (session_dir / "stream").mkdir() mgr = StreamManager.from_rust_session(session_dir) assert "4445" in mgr.relay_url def test_recorder_alive_returns_true_without_recorder(self, tmp_path): session_dir = tmp_path / "20260410_120000" session_dir.mkdir() (session_dir / "stream").mkdir() mgr = StreamManager.from_rust_session(session_dir) assert mgr.recorder_alive() is True