Files
Owner/tests/test_models_config.py
T

119 lines
4.5 KiB
Python

from __future__ import annotations
import tempfile
import unittest
from owner_voice_pet.config import AppConfig
from owner_voice_pet.models import (
AudioFrame,
AudioSegment,
ErrorCode,
Message,
PipelineState,
ProviderError,
)
class ModelsConfigTests(unittest.TestCase):
def test_audio_frame_validates_core_fields(self) -> None:
frame = AudioFrame(
pcm=b"\x00\x00",
sample_rate=16000,
channels=1,
timestamp_ms=10,
frame_id=1,
metadata={"wake": True},
)
self.assertEqual(frame.sample_rate, 16000)
self.assertTrue(frame.metadata["wake"])
def test_audio_segment_duration(self) -> None:
segment = AudioSegment(
pcm=b"\x00\x00\x01\x00",
sample_rate=16000,
channels=1,
start_time_ms=100,
end_time_ms=450,
)
self.assertEqual(segment.duration_ms, 350)
def test_invalid_audio_frame_rejected(self) -> None:
with self.assertRaises(ValueError):
AudioFrame(b"", 0, 1, 0, 0)
def test_provider_error_string_is_structured(self) -> None:
error = ProviderError(
ErrorCode.LLM_API_KEY_MISSING,
"missing key",
False,
"openai-compatible",
"llm",
)
self.assertIn("LLM_API_KEY_MISSING", str(error))
self.assertIn("llm/openai-compatible", str(error))
def test_config_from_dotenv_uses_file_values(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = f"{tmp}/.env"
with open(path, "w", encoding="utf-8") as handle:
handle.write(
"\n".join(
[
"OWNER_LLM_BASE_URL=https://token-plan-cn.xiaomimimo.com/v1/",
"OWNER_LLM_API_KEY=secret-value",
"OWNER_LLM_MODEL=test-model",
]
)
)
config = AppConfig.from_dotenv(path)
self.assertEqual(config.llm_base_url, "https://token-plan-cn.xiaomimimo.com/v1")
self.assertEqual(config.llm_api_key, "secret-value")
self.assertEqual(config.llm_model, "test-model")
self.assertEqual(config.speech_provider, "cloud")
self.assertEqual(config.asr_model, "mimo-v2.5-asr")
self.assertEqual(config.tts_model, "mimo-v2.5-tts")
self.assertEqual(config.tts_voice, "alloy")
self.assertEqual(str(config.speech_models_dir), "models")
self.assertTrue(config.llm_stream)
self.assertEqual(config.validate_basic(), [])
def test_speech_provider_must_be_cloud_or_local(self) -> None:
config = AppConfig(speech_provider="invalid")
errors = config.validate_basic()
self.assertTrue(any("OWNER_SPEECH_PROVIDER" in error.message for error in errors))
def test_missing_dotenv_uses_non_secret_defaults(self) -> None:
config = AppConfig.from_dotenv("/tmp/owner-voice-pet-missing.env")
self.assertEqual(config.llm_base_url, "https://token-plan-cn.xiaomimimo.com/v1")
self.assertIsNone(config.llm_api_key)
def test_api_url_accepts_base_with_or_without_v1(self) -> None:
with_v1 = AppConfig(llm_base_url="https://token-plan-cn.xiaomimimo.com/v1")
without_v1 = AppConfig(llm_base_url="https://newapi.mkbk.shop")
self.assertEqual(
with_v1.api_url("/v1/chat/completions"),
"https://token-plan-cn.xiaomimimo.com/v1/chat/completions",
)
self.assertEqual(
without_v1.api_url("/v1/chat/completions"),
"https://newapi.mkbk.shop/v1/chat/completions",
)
def test_missing_llm_key_has_structured_error(self) -> None:
config = AppConfig(llm_api_key=None)
with self.assertRaises(ProviderError) as raised:
config.require_llm_credentials()
self.assertEqual(raised.exception.code, ErrorCode.LLM_API_KEY_MISSING)
def test_pipeline_states_include_required_names(self) -> None:
self.assertEqual(PipelineState.WAKE_LISTENING.value, "wake_listening")
self.assertEqual(PipelineState.ERROR_RECOVERING.value, "error_recovering")
def test_message_model_accepts_roles(self) -> None:
message = Message(role="user", content="你好", created_at=1.0)
self.assertEqual(message.role, "user")
if __name__ == "__main__":
unittest.main()