Compare commits

..

2 Commits

Author SHA1 Message Date
CI Bot 50d74c454e style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 17s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m4s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m15s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m18s
AI Code Review / AI Code Review (pull_request) Successful in 1m19s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m22s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m27s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m57s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m22s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m23s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 4m12s
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m59s
CI/CD Pipeline / CI Gate (pull_request) Successful in 26s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 51s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 1169h8m54s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 1169h9m16s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 1169h11m48s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 1169h11m50s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 1169h11m52s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 1169h13m15s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 1169h13m16s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 1169h13m17s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 1169h20m35s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 1169h20m37s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1169h20m39s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1169h25m37s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1169h25m41s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 1169h53m41s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1169h58m41s
2026-07-29 13:57:35 +00:00
xiaoxia 098257efdc test(domain): add wave195 verification_code unit tests (+27)
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 33s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m35s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m29s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 42s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m6s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 2m9s
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
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 1169h31m42s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 1169h31m46s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 1170h4m46s
Add comprehensive unit tests for VerificationCode domain entity:
- create() factory: basic, stripped recipient, custom code/ttl, uniqueness
- is_expired / is_used / is_valid properties
- mark_used() and increment_attempts()
- Direct construction, slots, equality
2026-07-29 21:51:45 +08:00
2 changed files with 280 additions and 392 deletions
+280
View File
@@ -0,0 +1,280 @@
"""VerificationCode 单元测试."""
from __future__ import annotations
from datetime import datetime, timedelta, timezone
from unittest.mock import patch
import pytest
from domain.verification_code import VerificationCode
class TestVerificationCodeCreate:
"""create() 工厂方法测试."""
def test_create_basic(self):
vc = VerificationCode.create("test@example.com", "email_login")
assert vc.id is not None
assert len(vc.id) == 32
assert vc.recipient == "test@example.com"
assert vc.code_type == "email_login"
assert len(vc.code) == 6
assert vc.code.isdigit()
assert vc.used_at is None
assert vc.attempts == 0
assert vc.created_at is not None
assert vc.expires_at > vc.created_at
def test_create_recipient_stripped(self):
vc = VerificationCode.create(" test@example.com ", "email_login")
assert vc.recipient == "test@example.com"
def test_create_custom_code(self):
vc = VerificationCode.create("test@example.com", "email_login", custom_code="123456")
assert vc.code == "123456"
def test_create_custom_ttl(self):
fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
with patch("domain.verification_code.datetime") as mock_dt:
mock_dt.now.return_value = fixed_now
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
vc = VerificationCode.create("test@example.com", "email_login", ttl_seconds=60)
assert vc.expires_at == fixed_now + timedelta(seconds=60)
def test_create_default_ttl_300(self):
fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
with patch("domain.verification_code.datetime") as mock_dt:
mock_dt.now.return_value = fixed_now
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
vc = VerificationCode.create("test@example.com", "email_login")
assert vc.expires_at == fixed_now + timedelta(seconds=300)
def test_create_unique_ids(self):
vc1 = VerificationCode.create("a@b.com", "email_login")
vc2 = VerificationCode.create("a@b.com", "email_login")
assert vc1.id != vc2.id
def test_create_unique_codes(self):
codes = set()
for _ in range(20):
vc = VerificationCode.create("a@b.com", "email_login")
codes.add(vc.code)
# 20个随机6位码几乎肯定不都一样
assert len(codes) > 1
def test_create_phone_recipient(self):
vc = VerificationCode.create("13800138000", "phone_login")
assert vc.recipient == "13800138000"
assert vc.code_type == "phone_login"
def test_create_all_code_types(self):
for ct in ["email_bind", "phone_bind", "email_login", "phone_login", "reset_password"]:
vc = VerificationCode.create("test@example.com", ct)
assert vc.code_type == ct
class TestVerificationCodeIsExpired:
"""is_expired 属性测试."""
def test_not_expired_future(self):
future = datetime.now(timezone.utc) + timedelta(hours=1)
vc = VerificationCode(
id="1",
recipient="a@b.com",
code="123456",
code_type="email_login",
expires_at=future,
)
assert vc.is_expired is False
def test_expired_past(self):
past = datetime.now(timezone.utc) - timedelta(hours=1)
vc = VerificationCode(
id="1",
recipient="a@b.com",
code="123456",
code_type="email_login",
expires_at=past,
)
assert vc.is_expired is True
def test_expired_boundary_exact(self):
# 用mock固定时间,expires_at等于当前时间不算过期
fixed_now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
with patch("domain.verification_code.datetime") as mock_dt:
mock_dt.now.return_value = fixed_now
mock_dt.side_effect = lambda *a, **kw: datetime(*a, **kw)
vc = VerificationCode(
id="1",
recipient="a@b.com",
code="123456",
code_type="email_login",
expires_at=fixed_now,
)
assert vc.is_expired is False
class TestVerificationCodeIsUsed:
"""is_used 属性测试."""
def test_not_used_default(self):
vc = VerificationCode.create("a@b.com", "email_login")
assert vc.is_used is False
def test_is_used_after_mark(self):
vc = VerificationCode.create("a@b.com", "email_login")
vc.mark_used()
assert vc.is_used is True
class TestVerificationCodeIsValid:
"""is_valid 属性测试."""
def test_valid_fresh(self):
future = datetime.now(timezone.utc) + timedelta(hours=1)
vc = VerificationCode(
id="1",
recipient="a@b.com",
code="123456",
code_type="email_login",
expires_at=future,
)
assert vc.is_valid is True
def test_invalid_expired(self):
past = datetime.now(timezone.utc) - timedelta(hours=1)
vc = VerificationCode(
id="1",
recipient="a@b.com",
code="123456",
code_type="email_login",
expires_at=past,
)
assert vc.is_valid is False
def test_invalid_used(self):
future = datetime.now(timezone.utc) + timedelta(hours=1)
vc = VerificationCode(
id="1",
recipient="a@b.com",
code="123456",
code_type="email_login",
expires_at=future,
)
vc.mark_used()
assert vc.is_valid is False
def test_invalid_expired_and_used(self):
past = datetime.now(timezone.utc) - timedelta(hours=1)
vc = VerificationCode(
id="1",
recipient="a@b.com",
code="123456",
code_type="email_login",
expires_at=past,
)
vc.mark_used()
assert vc.is_valid is False
class TestVerificationCodeMarkUsed:
"""mark_used 方法测试."""
def test_mark_used_sets_timestamp(self):
vc = VerificationCode.create("a@b.com", "email_login")
assert vc.used_at is None
before = datetime.now(timezone.utc)
vc.mark_used()
after = datetime.now(timezone.utc)
assert vc.used_at is not None
assert before <= vc.used_at <= after
def test_mark_used_twice_overwrites(self):
vc = VerificationCode.create("a@b.com", "email_login")
vc.mark_used()
first = vc.used_at
# 时间足够短,一般不会不同,但确保可以重复调用
vc.mark_used()
assert vc.used_at is not None
class TestVerificationCodeIncrementAttempts:
"""increment_attempts 方法测试."""
def test_default_zero(self):
vc = VerificationCode.create("a@b.com", "email_login")
assert vc.attempts == 0
def test_increment_once(self):
vc = VerificationCode.create("a@b.com", "email_login")
vc.increment_attempts()
assert vc.attempts == 1
def test_increment_multiple(self):
vc = VerificationCode.create("a@b.com", "email_login")
for _i in range(5):
vc.increment_attempts()
assert vc.attempts == 5
class TestVerificationCodeBasics:
"""基础构造和 slots 测试."""
def test_direct_construction(self):
now = datetime.now(timezone.utc)
vc = VerificationCode(
id="abc123",
recipient="test@test.com",
code="000000",
code_type="email_bind",
expires_at=now + timedelta(minutes=5),
used_at=None,
attempts=0,
created_at=now,
)
assert vc.id == "abc123"
assert vc.recipient == "test@test.com"
assert vc.code == "000000"
def test_slots_no_extra_attrs(self):
vc = VerificationCode.create("a@b.com", "email_login")
with pytest.raises((AttributeError, TypeError)):
vc.new_field = "value" # type: ignore[attr-defined]
def test_equality_same_id(self):
now = datetime(2026, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
vc1 = VerificationCode(
id="same",
recipient="a@b.com",
code="111",
code_type="email_login",
expires_at=now,
created_at=now,
)
vc2 = VerificationCode(
id="same",
recipient="a@b.com",
code="111",
code_type="email_login",
expires_at=now,
created_at=now,
)
assert vc1 == vc2
def test_equality_different_id(self):
now = datetime.now(timezone.utc)
vc1 = VerificationCode(
id="id1",
recipient="a@b.com",
code="111",
code_type="email_login",
expires_at=now,
)
vc2 = VerificationCode(
id="id2",
recipient="a@b.com",
code="111",
code_type="email_login",
expires_at=now,
)
assert vc1 != vc2
-392
View File
@@ -1,392 +0,0 @@
"""video_concat 视频拼接配置单测."""
import pytest
from domain.video_concat import (
ALLOWED_VIDEO_EXTENSIONS,
CONCAT_DEMUXER_REQUIRED_PARAMS,
MAX_CONCAT_SEGMENTS,
ConcatConfig,
ConcatSegment,
)
# ── 常量测试 ─────────────────────────────────────────────────────────────────
class TestConstants:
"""模块常量"""
def test_max_concat_segments(self):
assert MAX_CONCAT_SEGMENTS == 50
def test_allowed_extensions(self):
assert ".mp4" in ALLOWED_VIDEO_EXTENSIONS
assert ".mov" in ALLOWED_VIDEO_EXTENSIONS
assert ".avi" in ALLOWED_VIDEO_EXTENSIONS
assert ".mkv" in ALLOWED_VIDEO_EXTENSIONS
assert ".webm" in ALLOWED_VIDEO_EXTENSIONS
assert ".flv" in ALLOWED_VIDEO_EXTENSIONS
assert ".wmv" in ALLOWED_VIDEO_EXTENSIONS
def test_concat_demuxer_params(self):
params = CONCAT_DEMUXER_REQUIRED_PARAMS
assert "codec_name" in params
assert "width" in params
assert "height" in params
assert "r_frame_rate" in params
assert "pix_fmt" in params
assert "sample_rate" in params
assert "channels" in params
assert "audio_codec" in params
assert len(params) == 8
# ── ConcatSegment ────────────────────────────────────────────────────────────
class TestConcatSegmentDefaults:
"""ConcatSegment 默认值"""
def test_required_path(self):
s = ConcatSegment(video_path="/video.mp4")
assert s.video_path == "/video.mp4"
assert s.start_time == 0.0
assert s.duration == 0.0
assert s.has_audio is True
def test_all_custom(self):
s = ConcatSegment(
video_path="/clip.mp4",
start_time=5.0,
duration=10.0,
has_audio=False,
)
assert s.video_path == "/clip.mp4"
assert s.start_time == 5.0
assert s.duration == 10.0
assert s.has_audio is False
class TestConcatSegmentFromDict:
"""ConcatSegment.from_dict"""
def test_none_returns_empty_path(self):
s = ConcatSegment.from_dict(None)
assert s.video_path == ""
assert s.is_valid is False
def test_empty_dict(self):
s = ConcatSegment.from_dict({})
assert s.video_path == ""
def test_not_dict(self):
s = ConcatSegment.from_dict("not a dict")
assert s.video_path == ""
def test_full_dict(self):
s = ConcatSegment.from_dict(
{
"video_path": "/clip.mp4",
"start_time": 2.5,
"duration": 15.0,
"has_audio": False,
}
)
assert s.video_path == "/clip.mp4"
assert s.start_time == 2.5
assert s.duration == 15.0
assert s.has_audio is False
def test_invalid_start_time_falls_back(self):
s = ConcatSegment.from_dict({"video_path": "/a.mp4", "start_time": "bad"})
assert s.start_time == 0.0
def test_negative_start_time_clamped(self):
s = ConcatSegment.from_dict({"video_path": "/a.mp4", "start_time": -5.0})
assert s.start_time == 0.0
def test_invalid_duration_falls_back(self):
s = ConcatSegment.from_dict({"video_path": "/a.mp4", "duration": None})
assert s.duration == 0.0
def test_negative_duration_clamped(self):
s = ConcatSegment.from_dict({"video_path": "/a.mp4", "duration": -10.0})
assert s.duration == 0.0
def test_has_audio_default_true(self):
s = ConcatSegment.from_dict({"video_path": "/a.mp4"})
assert s.has_audio is True
def test_has_audio_false(self):
s = ConcatSegment.from_dict({"video_path": "/a.mp4", "has_audio": False})
assert s.has_audio is False
def test_path_is_string(self):
s = ConcatSegment.from_dict({"video_path": 123})
assert s.video_path == "123"
class TestConcatSegmentProperties:
"""ConcatSegment 属性方法"""
def test_is_valid_true(self):
s = ConcatSegment(video_path="/a.mp4")
assert s.is_valid is True
def test_is_valid_false_empty(self):
s = ConcatSegment(video_path="")
assert s.is_valid is False
def test_effective_duration_positive(self):
s = ConcatSegment(video_path="/a.mp4", duration=10.0)
assert s.effective_duration == 10.0
def test_effective_duration_zero(self):
s = ConcatSegment(video_path="/a.mp4", duration=0.0)
assert s.effective_duration == 0.0
def test_effective_duration_negative(self):
s = ConcatSegment(video_path="/a.mp4", duration=-5.0)
assert s.effective_duration == 0.0
# ── ConcatConfig ─────────────────────────────────────────────────────────────
class TestConcatConfigDefaults:
"""ConcatConfig 默认值"""
def test_default_values(self):
c = ConcatConfig()
assert c.segments == []
assert c.output_width == 0
assert c.output_height == 0
assert c.output_fps == 0.0
assert c.force_reencode is False
assert c.transition == "none"
assert c.transition_duration == 0.3
class TestConcatConfigFromDict:
"""ConcatConfig.from_config_dict"""
def test_none_returns_default(self):
c = ConcatConfig.from_config_dict(None)
assert c.segments == []
def test_empty_dict_returns_default(self):
c = ConcatConfig.from_config_dict({})
assert c.segments == []
def test_not_dict_returns_default(self):
c = ConcatConfig.from_config_dict("config")
assert c.segments == []
def test_single_segment(self):
c = ConcatConfig.from_config_dict(
{
"segments": [
{"video_path": "/a.mp4", "duration": 10.0},
],
}
)
assert len(c.segments) == 1
assert c.segments[0].video_path == "/a.mp4"
def test_multiple_segments(self):
c = ConcatConfig.from_config_dict(
{
"segments": [
{"video_path": "/a.mp4", "duration": 10.0},
{"video_path": "/b.mp4", "duration": 20.0},
{"video_path": "/c.mp4", "duration": 15.0},
],
}
)
assert len(c.segments) == 3
def test_skip_no_path_segments(self):
c = ConcatConfig.from_config_dict(
{
"segments": [
{"video_path": "/a.mp4"},
{"video_path": ""},
{"duration": 5.0}, # 没有 video_path
{"video_path": "/b.mp4"},
],
}
)
assert len(c.segments) == 2
def test_skip_non_dict_segments(self):
c = ConcatConfig.from_config_dict(
{
"segments": [
{"video_path": "/a.mp4"},
"not a dict",
123,
None,
{"video_path": "/b.mp4"},
],
}
)
assert len(c.segments) == 2
def test_output_resolution(self):
c = ConcatConfig.from_config_dict(
{
"segments": [{"video_path": "/a.mp4"}],
"output_width": 1920,
"output_height": 1080,
}
)
assert c.output_width == 1920
assert c.output_height == 1080
def test_output_width_clamped(self):
c = ConcatConfig.from_config_dict({"output_width": -100})
assert c.output_width == 0
def test_invalid_output_width_falls_back(self):
c = ConcatConfig.from_config_dict({"output_width": "wide"})
assert c.output_width == 0
def test_output_fps(self):
c = ConcatConfig.from_config_dict({"output_fps": 30.0})
assert c.output_fps == 30.0
def test_output_fps_clamped(self):
c = ConcatConfig.from_config_dict({"output_fps": -1.0})
assert c.output_fps == 0.0
def test_invalid_output_fps_falls_back(self):
c = ConcatConfig.from_config_dict({"output_fps": "fast"})
assert c.output_fps == 0.0
def test_force_reencode_true(self):
c = ConcatConfig.from_config_dict({"force_reencode": True})
assert c.force_reencode is True
def test_transition_crossfade(self):
c = ConcatConfig.from_config_dict({"transition": "crossfade"})
assert c.transition == "crossfade"
def test_transition_duration(self):
c = ConcatConfig.from_config_dict({"transition_duration": 1.0})
assert c.transition_duration == 1.0
def test_transition_duration_min_clamped(self):
c = ConcatConfig.from_config_dict({"transition_duration": 0.01})
# max(0.1, 0.01) = 0.1
assert c.transition_duration == 0.1
# 代码里 transition_duration = max(0.1, ...),默认 0.3
# 0.01 < 0.1 ,所以被钳制到 0.1
def test_invalid_transition_duration_falls_back(self):
c = ConcatConfig.from_config_dict({"transition_duration": "long"})
assert c.transition_duration == 0.3
def test_segments_not_list_ignored(self):
c = ConcatConfig.from_config_dict({"segments": "not a list"})
assert c.segments == []
class TestConcatConfigProperties:
"""ConcatConfig 属性方法"""
def _make_config(self, n=3):
return ConcatConfig.from_config_dict(
{
"segments": [{"video_path": f"/s{i}.mp4", "duration": 10.0 + i} for i in range(n)],
}
)
def test_has_effect_true(self):
c = self._make_config(3)
assert c.has_effect is True
def test_has_effect_false_one_segment(self):
c = self._make_config(1)
assert c.has_effect is False
def test_has_effect_false_empty(self):
c = ConcatConfig()
assert c.has_effect is False
def test_valid_segment_count(self):
c = self._make_config(5)
assert c.valid_segment_count == 5
def test_total_segments_alias(self):
c = self._make_config(4)
assert c.total_segments == 4
assert c.total_segments == c.valid_segment_count
def test_first_valid_segment(self):
c = self._make_config(3)
first = c.first_valid_segment
assert first is not None
assert first.video_path == "/s0.mp4"
def test_first_valid_segment_empty(self):
c = ConcatConfig()
assert c.first_valid_segment is None
def test_estimated_total_duration(self):
c = ConcatConfig(
segments=[
ConcatSegment("/a.mp4", duration=10.0),
ConcatSegment("/b.mp4", duration=20.0),
ConcatSegment("/c.mp4", duration=0.0), # 不计入
]
)
assert c.estimated_total_duration == 30.0
def test_estimated_total_duration_empty(self):
c = ConcatConfig()
assert c.estimated_total_duration == 0.0
def test_clamp_segments_within_limit(self):
c = self._make_config(10)
original = len(c.segments)
c.clamp_segments(max_segments=50)
assert len(c.segments) == original
def test_clamp_segments_over_limit(self):
c = self._make_config(10)
c.clamp_segments(max_segments=3)
assert len(c.segments) == 3
assert c.segments[0].video_path == "/s0.mp4"
assert c.segments[2].video_path == "/s2.mp4"
def test_clamp_segments_default_max(self):
# 默认应该是 MAX_CONCAT_SEGMENTS
c = ConcatConfig(segments=[ConcatSegment(f"/s{i}.mp4") for i in range(100)])
c.clamp_segments()
assert len(c.segments) == MAX_CONCAT_SEGMENTS
class TestTransitionDurationClamp:
"""transition_duration 钳制边界"""
def test_min_boundary_01(self):
c = ConcatConfig.from_config_dict({"transition_duration": 0.1})
assert c.transition_duration == 0.1
def test_below_min_clamped(self):
c = ConcatConfig.from_config_dict({"transition_duration": 0.05})
# max(0.1, 0.05) = 0.1
assert c.transition_duration == 0.1
def test_large_duration_ok(self):
c = ConcatConfig.from_config_dict({"transition_duration": 5.0})
assert c.transition_duration == 5.0
def test_zero_clamped(self):
c = ConcatConfig.from_config_dict({"transition_duration": 0.0})
# max(0.1, 0.0) = 0.1
assert c.transition_duration == 0.1
def test_negative_clamped(self):
c = ConcatConfig.from_config_dict({"transition_duration": -1.0})
# max(0.1, -1.0) = 0.1
assert c.transition_duration == 0.1