import shutil import zipfile import subprocess from pathlib import Path, PurePosixPath import pytest import reachy_mini_conversation_app.config as config_mod import reachy_mini_conversation_app.prompts as prompts_mod import reachy_mini_conversation_app.headless_personality as headless_mod from reachy_mini_conversation_app.config import DEFAULT_PROFILES_DIRECTORY, config from reachy_mini_conversation_app.gradio_personality import PersonalityUI from reachy_mini_conversation_app.headless_personality import ( DEFAULT_OPTION, read_tools_for, resolve_profile_dir, read_instructions_for, ) # Path characters budget computation # ───────────────── # Windows MAX_PATH limit: 259 usable characters (failures start at 260) # # Project files (WINDOWS_PATH_BUDGET = 130): # C:\Users\ # \.cache\huggingface\hub # \spaces--pollen-robotics--reachy_mini_conversation_app # \snapshots\\ # = 158 characters => 101 remaining to 259. # The project root folder is not cloned in the snapshot, so we add it # back to the budget: 101 + len("reachy_mini_conversation_app\") (29) = 130. # # Wheel files (WINDOWS_WHEEL_PATH_BUDGET = 71): # C:\Users\ # \.cache\huggingface\hub # \spaces--pollen-robotics--reachy_mini_conversation_app # \snapshots\ # \build\bdist.win-amd64\wheel\ # = 186 characters => 73 remaining to 259. # In practice the copy fails at 257 because of an intermediate \.\ # folder, bringing the real budget down to 71. WINDOWS_PATH_BUDGET = 130 WINDOWS_WHEEL_PATH_BUDGET = 71 def _git_tracked_files(project_root: Path) -> list[Path]: """Return git-tracked files that still exist in the working tree.""" try: result = subprocess.run( ["git", "ls-files"], cwd=project_root, check=True, capture_output=True, text=True, ) except (OSError, subprocess.CalledProcessError) as exc: pytest.skip(f"git-tracked file listing unavailable: {exc}") tracked_files = [project_root / relative_path for relative_path in result.stdout.splitlines() if relative_path] return [path for path in tracked_files if path.is_file()] def test_profile_name_resolves_directly_to_storage_dir() -> None: """Built-in profile names should map directly to their on-disk directory.""" profile_dir = resolve_profile_dir("mad_scientist_assistant") assert profile_dir.name == "mad_scientist_assistant" assert (profile_dir / "instructions.txt").is_file() def test_prompts_load_from_compact_builtin_profile(monkeypatch: pytest.MonkeyPatch) -> None: """Prompt loading should read compact built-in profile instructions directly.""" monkeypatch.setattr(config, "REACHY_MINI_CUSTOM_PROFILE", "mad_scientist_assistant") monkeypatch.setattr(config, "PROFILES_DIRECTORY", DEFAULT_PROFILES_DIRECTORY) expected = ( (DEFAULT_PROFILES_DIRECTORY / "mad_scientist_assistant" / "instructions.txt") .read_text(encoding="utf-8") .strip() ) assert prompts_mod.get_session_instructions() == expected assert read_instructions_for("mad_scientist_assistant") == expected def test_builtin_default_profile_tools_load_for_ui() -> None: """The UI should read built-in default tools from the packaged default profile.""" expected = (DEFAULT_PROFILES_DIRECTORY / "default" / "tools.txt").read_text(encoding="utf-8") assert read_tools_for(DEFAULT_OPTION) == expected def test_gradio_personality_ui_prefills_builtin_default_tools(monkeypatch: pytest.MonkeyPatch) -> None: """Gradio should show the built-in default profile tools on first render.""" monkeypatch.setattr(config, "REACHY_MINI_CUSTOM_PROFILE", None) ui = PersonalityUI() ui.create_components() expected_tools = read_tools_for(ui.DEFAULT_OPTION) expected_enabled = [ line.strip() for line in expected_tools.splitlines() if line.strip() and not line.strip().startswith("#") ] assert ui.tools_txt_ta.value == expected_tools assert sorted(ui.available_tools_cg.value) == sorted(expected_enabled) def test_session_voice_defaults_follow_selected_backend(monkeypatch: pytest.MonkeyPatch) -> None: """Session voice should fall back to the active backend default.""" monkeypatch.setattr(config, "BACKEND_PROVIDER", "gemini") monkeypatch.setattr(config, "MODEL_NAME", "gemini-3.1-flash-live-preview") monkeypatch.setattr(config, "REACHY_MINI_CUSTOM_PROFILE", None) assert prompts_mod.get_session_voice() == "Kore" def test_headless_profile_write_defaults_voice_at_call_time( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, ) -> None: """New headless profiles should use the currently selected backend default voice.""" monkeypatch.setattr(config, "BACKEND_PROVIDER", "gemini") monkeypatch.setattr(config, "MODEL_NAME", "gemini-3.1-flash-live-preview") monkeypatch.setattr(headless_mod, "_profiles_root", lambda: tmp_path) headless_mod._write_profile("runtime_voice_default", "test instructions", "") voice_file = tmp_path / "user_personalities" / "runtime_voice_default" / "voice.txt" assert voice_file.read_text(encoding="utf-8") == "Kore\n" def test_packaged_profiles_win_outside_source_checkout(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: """Installed builds should use packaged profiles, not an unrelated sibling folder.""" unrelated_profiles = tmp_path / "profiles" unrelated_profiles.mkdir() packaged_profiles = tmp_path / "package_data" / "profiles" packaged_profiles.mkdir(parents=True) monkeypatch.setattr(config_mod, "PROJECT_ROOT", tmp_path) monkeypatch.setattr(config_mod, "_packaged_profiles_directory", lambda: packaged_profiles) assert config_mod._resolve_default_profiles_directory() == packaged_profiles def test_project_file_paths_stay_within_windows_budget() -> None: """Git-tracked project file paths should stay below the agreed Windows budget.""" project_root = Path(__file__).parents[1].resolve() project_files = _git_tracked_files(project_root) violations = [] for path in project_files: relative = str(Path(project_root.name) / path.relative_to(project_root)) length = len(relative) if length > WINDOWS_PATH_BUDGET: violations.append( f"Windows path budget exceeded ({WINDOWS_PATH_BUDGET}): {relative} is {length} characters long" ) assert not violations, "\n".join(violations) def test_wheel_file_paths_stay_within_windows_budget(tmp_path: Path) -> None: """Built wheel paths should stay below the agreed Windows budget.""" project_root = Path(__file__).parents[1].resolve() source_checkout = tmp_path / "checkout" dist_dir = tmp_path / "dist" for source_file in _git_tracked_files(project_root): target_file = source_checkout / source_file.relative_to(project_root) target_file.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(source_file, target_file) try: subprocess.run( ["uv", "build", "--wheel", "--out-dir", str(dist_dir)], cwd=source_checkout, check=True, capture_output=True, text=True, ) except (OSError, subprocess.CalledProcessError) as exc: details = exc.stderr if isinstance(exc, subprocess.CalledProcessError) and exc.stderr else str(exc) pytest.fail(f"Wheel build failed while checking Windows path budget: {details}") wheel_files = list(dist_dir.glob("*.whl")) assert len(wheel_files) == 1, f"Expected exactly one built wheel in {dist_dir}, found: {wheel_files}" with zipfile.ZipFile(wheel_files[0]) as archive: archived_paths = [PurePosixPath(info.filename) for info in archive.infolist() if not info.is_dir()] violations = [] for path in archived_paths: length = len(path.as_posix()) if length > WINDOWS_WHEEL_PATH_BUDGET: violations.append( f"Windows wheel path budget exceeded ({WINDOWS_WHEEL_PATH_BUDGET}): " f"{path.as_posix()} is {length} characters long" ) assert not violations, "\n".join(violations)