Compare commits

...

2 Commits

Author SHA1 Message Date
CI Bot 2a1cc95682 style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 19s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m9s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m8s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 41s
AI Code Review / AI Code Review (pull_request) Successful in 1m3s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m17s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m19s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m50s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 5m33s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 5m34s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 5m51s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m39s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 46s
2026-07-29 13:51:25 +00:00
xiaoxia a4bdc0b2b4 test(domain): add wave194 tts_config unit tests (+52)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 22s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m32s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m38s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
Add comprehensive unit tests for TtsConfig domain entity:
- Default values and custom construction
- parse() with None/empty/non-dict inputs
- parse() disabled scenarios (enabled=False, type fallback)
- parse() normal data with type coercion
- parse() type fallback for all fields
- parse() boundary clamping (speed/pitch/volume)
- parse() align_mode and overlap_mode validation
- _clamp() direct calls for all three fields
2026-07-29 21:48:14 +08:00
+314
View File
@@ -0,0 +1,314 @@
"""TtsConfig 单元测试."""
from __future__ import annotations
import pytest
from domain.tts_config import TtsConfig
class TestTtsConfigDefaults:
"""默认值测试."""
def test_default_values(self):
config = TtsConfig()
assert config.enabled is False
assert config.voice_id == ""
assert config.speed == 1.0
assert config.pitch == 0.0
assert config.volume == 0.8
assert config.text == ""
assert config.align_mode == "full"
assert config.overlap_mode == "replace"
def test_custom_construction(self):
config = TtsConfig(
enabled=True,
voice_id="voice_001",
speed=1.5,
pitch=3.0,
volume=0.9,
text="hello",
align_mode="subtitle",
overlap_mode="mix",
)
assert config.enabled is True
assert config.voice_id == "voice_001"
assert config.speed == 1.5
assert config.pitch == 3.0
assert config.volume == 0.9
assert config.text == "hello"
assert config.align_mode == "subtitle"
assert config.overlap_mode == "mix"
def test_slots_no_extra_attrs(self):
config = TtsConfig()
with pytest.raises((AttributeError, TypeError)):
config.new_attr = "value" # type: ignore[attr-defined]
def test_equality_same_values(self):
a = TtsConfig(enabled=True, voice_id="v1")
b = TtsConfig(enabled=True, voice_id="v1")
assert a == b
def test_equality_different_values(self):
a = TtsConfig(enabled=True)
b = TtsConfig(enabled=False)
assert a != b
class TestTtsConfigParseNoneAndEmpty:
"""parse 空输入测试."""
def test_parse_none(self):
config = TtsConfig.parse(None)
assert config == TtsConfig()
def test_parse_empty_dict(self):
config = TtsConfig.parse({})
assert config == TtsConfig()
def test_parse_non_dict_string(self):
config = TtsConfig.parse("not a dict") # type: ignore[arg-type]
assert config == TtsConfig()
def test_parse_non_dict_list(self):
config = TtsConfig.parse([]) # type: ignore[arg-type]
assert config == TtsConfig()
def test_parse_non_dict_number(self):
config = TtsConfig.parse(123) # type: ignore[arg-type]
assert config == TtsConfig()
class TestTtsConfigParseDisabled:
"""parse disabled 场景."""
def test_parse_enabled_false_returns_default(self):
config = TtsConfig.parse({"enabled": False})
assert config.enabled is False
assert config.speed == 1.0
assert config.voice_id == ""
def test_parse_enabled_false_ignores_other_fields(self):
config = TtsConfig.parse(
{
"enabled": False,
"voice_id": "v1",
"speed": 1.5,
}
)
assert config.enabled is False
assert config.voice_id == ""
assert config.speed == 1.0
def test_parse_enabled_non_bool_falls_to_false(self):
config = TtsConfig.parse({"enabled": "true"})
assert config.enabled is False
def test_parse_enabled_int_falls_to_false(self):
config = TtsConfig.parse({"enabled": 1})
assert config.enabled is False
class TestTtsConfigParseNormal:
"""parse 正常数据测试."""
def test_parse_full_data(self):
data = {
"enabled": True,
"voice_id": "voice_001",
"speed": 1.5,
"pitch": 2.5,
"volume": 0.7,
"text": "你好世界",
"align_mode": "subtitle",
"overlap_mode": "mix",
}
config = TtsConfig.parse(data)
assert config.enabled is True
assert config.voice_id == "voice_001"
assert config.speed == 1.5
assert config.pitch == 2.5
assert config.volume == 0.7
assert config.text == "你好世界"
assert config.align_mode == "subtitle"
assert config.overlap_mode == "mix"
def test_parse_int_speed_becomes_float(self):
config = TtsConfig.parse({"enabled": True, "speed": 2})
assert isinstance(config.speed, float)
assert config.speed == 2.0
def test_parse_int_pitch_becomes_float(self):
config = TtsConfig.parse({"enabled": True, "pitch": -3})
assert isinstance(config.pitch, float)
assert config.pitch == -3.0
class TestTtsConfigParseTypeFallback:
"""parse 类型错误回退测试."""
def test_parse_voice_id_non_string_fallback(self):
config = TtsConfig.parse({"enabled": True, "voice_id": 123})
assert config.voice_id == ""
def test_parse_speed_non_numeric_fallback(self):
config = TtsConfig.parse({"enabled": True, "speed": "fast"})
assert config.speed == 1.0
def test_parse_pitch_non_numeric_fallback(self):
config = TtsConfig.parse({"enabled": True, "pitch": "high"})
assert config.pitch == 0.0
def test_parse_volume_non_numeric_fallback(self):
config = TtsConfig.parse({"enabled": True, "volume": "loud"})
assert config.volume == 0.8
def test_parse_text_non_string_fallback(self):
config = TtsConfig.parse({"enabled": True, "text": 456})
assert config.text == ""
def test_parse_voice_id_list_fallback(self):
config = TtsConfig.parse({"enabled": True, "voice_id": ["v1"]})
assert config.voice_id == ""
class TestTtsConfigParseClamp:
"""parse 边界钳制测试."""
def test_parse_speed_below_min_clamped(self):
config = TtsConfig.parse({"enabled": True, "speed": 0.1})
assert config.speed == 0.5
def test_parse_speed_above_max_clamped(self):
config = TtsConfig.parse({"enabled": True, "speed": 3.0})
assert config.speed == 2.0
def test_parse_speed_at_min_ok(self):
config = TtsConfig.parse({"enabled": True, "speed": 0.5})
assert config.speed == 0.5
def test_parse_speed_at_max_ok(self):
config = TtsConfig.parse({"enabled": True, "speed": 2.0})
assert config.speed == 2.0
def test_parse_pitch_below_min_clamped(self):
config = TtsConfig.parse({"enabled": True, "pitch": -20})
assert config.pitch == -12
def test_parse_pitch_above_max_clamped(self):
config = TtsConfig.parse({"enabled": True, "pitch": 20})
assert config.pitch == 12
def test_parse_pitch_at_min_ok(self):
config = TtsConfig.parse({"enabled": True, "pitch": -12})
assert config.pitch == -12
def test_parse_pitch_at_max_ok(self):
config = TtsConfig.parse({"enabled": True, "pitch": 12})
assert config.pitch == 12
def test_parse_volume_below_min_clamped(self):
config = TtsConfig.parse({"enabled": True, "volume": -0.5})
assert config.volume == 0.0
def test_parse_volume_above_max_clamped(self):
config = TtsConfig.parse({"enabled": True, "volume": 1.5})
assert config.volume == 1.0
def test_parse_volume_at_min_ok(self):
config = TtsConfig.parse({"enabled": True, "volume": 0.0})
assert config.volume == 0.0
def test_parse_volume_at_max_ok(self):
config = TtsConfig.parse({"enabled": True, "volume": 1.0})
assert config.volume == 1.0
class TestTtsConfigParseAlignMode:
"""align_mode 解析测试."""
def test_parse_align_mode_subtitle(self):
config = TtsConfig.parse({"enabled": True, "align_mode": "subtitle"})
assert config.align_mode == "subtitle"
def test_parse_align_mode_full(self):
config = TtsConfig.parse({"enabled": True, "align_mode": "full"})
assert config.align_mode == "full"
def test_parse_align_mode_invalid_fallback(self):
config = TtsConfig.parse({"enabled": True, "align_mode": "auto"})
assert config.align_mode == "full"
def test_parse_align_mode_empty_fallback(self):
config = TtsConfig.parse({"enabled": True, "align_mode": ""})
assert config.align_mode == "full"
class TestTtsConfigParseOverlapMode:
"""overlap_mode 解析测试."""
def test_parse_overlap_mode_replace(self):
config = TtsConfig.parse({"enabled": True, "overlap_mode": "replace"})
assert config.overlap_mode == "replace"
def test_parse_overlap_mode_mix(self):
config = TtsConfig.parse({"enabled": True, "overlap_mode": "mix"})
assert config.overlap_mode == "mix"
def test_parse_overlap_mode_invalid_fallback(self):
config = TtsConfig.parse({"enabled": True, "overlap_mode": "add"})
assert config.overlap_mode == "replace"
def test_parse_overlap_mode_empty_fallback(self):
config = TtsConfig.parse({"enabled": True, "overlap_mode": ""})
assert config.overlap_mode == "replace"
class TestTtsConfigClamp:
"""_clamp 直接调用测试."""
def test_clamp_speed_low(self):
config = TtsConfig(enabled=True, speed=0.1)
config._clamp()
assert config.speed == 0.5
def test_clamp_speed_high(self):
config = TtsConfig(enabled=True, speed=5.0)
config._clamp()
assert config.speed == 2.0
def test_clamp_speed_normal_unchanged(self):
config = TtsConfig(enabled=True, speed=1.2)
config._clamp()
assert config.speed == 1.2
def test_clamp_pitch_low(self):
config = TtsConfig(enabled=True, pitch=-20)
config._clamp()
assert config.pitch == -12
def test_clamp_pitch_high(self):
config = TtsConfig(enabled=True, pitch=20)
config._clamp()
assert config.pitch == 12
def test_clamp_pitch_normal_unchanged(self):
config = TtsConfig(enabled=True, pitch=5.0)
config._clamp()
assert config.pitch == 5.0
def test_clamp_volume_low(self):
config = TtsConfig(enabled=True, volume=-1.0)
config._clamp()
assert config.volume == 0.0
def test_clamp_volume_high(self):
config = TtsConfig(enabled=True, volume=2.0)
config._clamp()
assert config.volume == 1.0
def test_clamp_volume_normal_unchanged(self):
config = TtsConfig(enabled=True, volume=0.5)
config._clamp()
assert config.volume == 0.5