import importlib import sys import types from unittest import mock import pytest def import_app_with_gradio_stub(): sys.modules.pop("app", None) sys.modules["gradio"] = types.ModuleType("gradio") return importlib.import_module("app") def import_app_without_gradio(): original_import = __import__ def blocked_import(name, globals=None, locals=None, fromlist=(), level=0): if name == "gradio": raise ModuleNotFoundError("No module named 'gradio'") return original_import(name, globals, locals, fromlist, level) sys.modules.pop("app", None) sys.modules.pop("gradio", None) with mock.patch("builtins.__import__", side_effect=blocked_import): return importlib.import_module("app") def test_normalize_rhythm_override_treats_automatic_as_none(): app = import_app_with_gradio_stub() assert app.normalize_rhythm_override("Automatic") is None def test_session_examples_leave_tone_center_blank_for_automatic_mode(): app = import_app_with_gradio_stub() assert all(example[2] is None for example in app.SESSION_EXAMPLES) def test_build_session_copy_uses_human_labels(): app = import_app_with_gradio_stub() analysis = { "emotional_state": "anxious", "session_profile": { "title": "Grounding Tide", "emotional_tone": "Settling and steady", "guidance": "Let your breath fall behind the pulse until the session feels steady.", "reflection": "This session favors stability over intensity.", }, } card = app.build_session_copy(analysis) assert card["session_name"] == "Grounding Tide" assert card["listening_path"].startswith("Settling and steady") assert "stability over intensity" in card["session_reflection"] def test_import_without_gradio_uses_interface_guard(): app = import_app_without_gradio() assert app.gr is None with pytest.raises(ModuleNotFoundError, match="gradio is required"): app.create_interface() def test_rhythma_experience_returns_session_led_copy(monkeypatch): app = import_app_with_gradio_stub() analysis = { "emotional_state": "anxious", "transcription": "", "session_profile": { "title": "Grounding Tide", "emotional_tone": "Settling and steady", "guidance": "Let your breath fall behind the pulse until the session feels steady.", "reflection": "This session favors stability over intensity.", }, } monkeypatch.setattr(app, "analyze_input", lambda text, audio: analysis) monkeypatch.setattr( app, "generate_modulated_experience", lambda *args, **kwargs: ("legacy analysis", "session.wav", "plot", "image", "legacy symbolic"), ) outputs = app.rhythma_experience( "I feel anxious and need to settle down", None, override_freq=0, override_modulation="sine", override_rhythm="Automatic", duration=5, ) assert outputs[0] == "### Grounding Tide" assert outputs[1] == "Settling and steady" assert outputs[2].startswith("Settling and steady") assert outputs[3] == "session.wav" assert outputs[6] == "This session favors stability over intensity." assert outputs[7] == "" def test_rhythma_experience_degrades_copy_consistently_on_generation_failure(monkeypatch): app = import_app_with_gradio_stub() analysis = { "emotional_state": "anxious", "transcription": "", "session_profile": { "title": "Grounding Tide", "emotional_tone": "Settling and steady", "guidance": "Let your breath fall behind the pulse until the session feels steady.", "reflection": "This session favors stability over intensity.", }, } monkeypatch.setattr(app, "analyze_input", lambda text, audio: analysis) monkeypatch.setattr( app, "generate_modulated_experience", lambda *args, **kwargs: ("Generation unavailable", None, None, None, "legacy symbolic"), ) outputs = app.rhythma_experience( "I feel anxious and need to settle down", None, override_freq=0, override_modulation="sine", override_rhythm="Automatic", duration=5, ) assert outputs[2] == "Generation unavailable" assert outputs[6] == "Generation unavailable" def test_rhythma_experience_uses_real_session_profile_across_pipeline(monkeypatch): app = import_app_with_gradio_stub() captured = {} class FakeEngine: def __init__(self, **kwargs): captured["init_kwargs"] = kwargs self.generated_audio = None def render_session(self, profile, duration): captured["render_profile"] = profile captured["render_duration"] = duration return "layered-session-audio" def generate_modulated_wave(self, duration): captured["generated_duration"] = duration return "legacy-generated-audio" def save_audio(self, duration, file_path): captured["save_duration"] = duration captured["save_path"] = file_path self.generated_audio = self.generate_modulated_wave(duration) return file_path def visualize_waveform(self, duration): captured["visualize_duration"] = duration return "plot" def get_waveform_image(self): return "image" def get_complete_analysis(self): return "legacy analysis" def get_symbolic_interpretation(self): return "legacy symbolic" monkeypatch.setattr(app, "RhythmaModulationEngine", FakeEngine) outputs = app.rhythma_experience( "I want to focus on deep work", None, override_freq=0, override_modulation="sine", override_rhythm="Automatic", duration=5, ) assert outputs[0] == "### Clear Horizon" assert outputs[1] == "Attentive and composed" assert outputs[2].startswith("Attentive and composed") assert outputs[6] == "This session narrows motion to support sustained attention." assert outputs[7] == "" assert captured["init_kwargs"] == { "base_freq": 512.0, "modulation_type": "sine", "rhythm_pattern": "focused", "emotional_state": None, } assert captured["render_profile"]["title"] == "Clear Horizon" assert captured["render_profile"]["tone_center"] == 512.0 assert captured["render_profile"]["pattern"] == "focused" assert captured["render_duration"] == 5 assert captured["save_duration"] == 5 assert captured["visualize_duration"] == 5 def test_generate_modulated_experience_prefers_explicit_rhythm_override_without_session_profile(monkeypatch): app = import_app_with_gradio_stub() captured = {} class FakeEngine: def __init__(self, **kwargs): captured["init_kwargs"] = kwargs def save_audio(self, duration, file_path): return file_path def visualize_waveform(self, duration): return "plot" def get_waveform_image(self): return "image" def get_complete_analysis(self): return "legacy analysis" def get_symbolic_interpretation(self): return "legacy symbolic" monkeypatch.setattr(app, "RhythmaModulationEngine", FakeEngine) analysis = { "emotional_state": "neutral", "rhythm_pattern": "calm", "transcription": "", } result = app.generate_modulated_experience( analysis, modulation_type="sine", rhythm_pattern="focused", duration=5, ) assert result[0] == "legacy analysis" assert captured["init_kwargs"]["rhythm_pattern"] == "focused"