from __future__ import annotations import io import json import tempfile import unittest from contextlib import redirect_stdout from pathlib import Path from unittest.mock import patch from owner_voice_pet.cli import main from owner_voice_pet.speech_models import REQUIRED_MODEL_FILES, write_default_manifest class CliAcceptanceTests(unittest.TestCase): def call(self, *args: str) -> tuple[int, dict[str, object]]: buffer = io.StringIO() with redirect_stdout(buffer): code = main(list(args)) return code, json.loads(buffer.getvalue()) def test_acceptance_command_runs_end_to_end(self) -> None: code, data = self.call("acceptance") self.assertEqual(code, 0) self.assertTrue(data["success"]) self.assertEqual(data["transcript"], "你是谁") self.assertGreaterEqual(data["played_segments"], 1) def test_validate_assets_command(self) -> None: code, data = self.call("validate-assets") self.assertEqual(code, 0) self.assertTrue(data["valid"]) def test_show_config_reads_env_file_without_printing_secret(self) -> None: import tempfile with tempfile.TemporaryDirectory() as tmp: path = f"{tmp}/.env" with open(path, "w", encoding="utf-8") as handle: handle.write("OWNER_LLM_API_KEY=secret-value\nOWNER_LLM_MODEL=file-model\n") code, data = self.call("--env-file", path, "--show-config") self.assertEqual(code, 0) self.assertTrue(data["llm_api_key_present"]) self.assertEqual(data["llm_model"], "file-model") self.assertEqual(data["pipeline_mode"], "live_turn_based") self.assertEqual(data["endpoint_mode"], "primary_speaker") self.assertEqual(data["context_mode"], "session_memory") self.assertNotIn("secret-value", str(data)) def test_security_check_command_has_no_leaks(self) -> None: code, data = self.call("security-check") self.assertEqual(code, 0) self.assertEqual(data["secret_leaks"], []) def test_model_check_reports_manifest_and_files(self) -> None: with tempfile.TemporaryDirectory() as tmp: root = Path(tmp) write_default_manifest(root) for relative in REQUIRED_MODEL_FILES: path = root / relative path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(b"placeholder") with ( patch("importlib.util.find_spec", return_value=object()), patch("owner_voice_pet.cli.SherpaOnnxKeywordWakeWordProvider") as wake_cls, patch("owner_voice_pet.cli.SherpaOnnxVadProvider") as vad_cls, patch("owner_voice_pet.cli.SherpaOnnxSttProvider") as stt_cls, patch("owner_voice_pet.cli.SherpaOnnxDenoiserPreprocessor") as denoiser_cls, ): wake_cls.return_value.load.return_value = None vad_cls.return_value.load.return_value = None stt_cls.return_value.load.return_value = None denoiser_cls.return_value.load.return_value = None code, data = self.call("model-check", "--models-dir", str(root)) self.assertEqual(code, 0) self.assertTrue(data["ok"]) self.assertEqual(data["missing_files"], []) self.assertTrue(data["provider_load_checked"]) def test_model_check_reports_missing_files(self) -> None: with tempfile.TemporaryDirectory() as tmp: with patch("importlib.util.find_spec", return_value=object()): code, data = self.call("model-check", "--models-dir", tmp) self.assertEqual(code, 1) self.assertFalse(data["ok"]) self.assertTrue(data["missing_files"]) def test_device_check_reports_sounddevice_status(self) -> None: with patch("owner_voice_pet.cli.sounddevice_device_report", return_value={"ok": True, "devices": []}): code, data = self.call("device-check") self.assertEqual(code, 0) self.assertTrue(data["ok"]) def test_run_live_once_invokes_runtime(self) -> None: class FakeRuntime: def run(self, *, once: bool = False): self.once = once return type("Summary", (), {"completed_turns": 1, "interrupted": False})() fake_runtime = FakeRuntime() with patch("owner_voice_pet.cli.build_live_runtime", return_value=fake_runtime): code = main(["run-live", "--once"]) self.assertEqual(code, 0) self.assertTrue(fake_runtime.once) def test_simulate_live_command_runs_two_turns(self) -> None: code, data = self.call("simulate-live", "--turns", "2") self.assertEqual(code, 0) self.assertTrue(data["success"]) self.assertEqual(data["completed_turns"], 2) self.assertEqual(data["final_transcripts"], ["第1轮模拟问题", "第2轮模拟问题"]) self.assertTrue(data["checks"]["partial_noise_filtered"]) if __name__ == "__main__": unittest.main()