1a57878f76
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 1m12s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m17s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 1m33s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 3m14s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
1. 未使用依赖清理:
- 从 requirements-base.txt 移除 cryptography 和 pyOpenSSL
2. pyflakes 警告清零 (apps/ + packages/ + tests/):
- 移除 17 处未使用的 import (F401)
- 修复 26 处未使用的局部变量 (F841):
* 有副作用的赋值转为裸调用
* 无副作用的赋值直接删除
- 修复 1 处未使用的异常变量 (F841)
- 修复 1 处空 except 块
3. 测试文件冗余清理:
- 删除 tests/integration/test_project_management.py (模块级 skip,测试不存在的模块)
- 删除 tests/integration/fixtures/duplication_routes_fixed.py (未被引用)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
599 lines
22 KiB
Python
Executable File
599 lines
22 KiB
Python
Executable File
"""CosyVoiceService 单元测试 — 适配百炼 DashScope API."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from unittest.mock import MagicMock
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from packages.application.cosyvoice_service import (
|
|
CloneResult,
|
|
CosyVoiceAuthError,
|
|
CosyVoiceError,
|
|
CosyVoiceService,
|
|
CosyVoiceTimeoutError,
|
|
SynthesizeResult,
|
|
)
|
|
from packages.domain.preset_voices import PresetVoice
|
|
|
|
|
|
def _make_service(
|
|
*,
|
|
api_key: str = "test-api-key",
|
|
base_url: str = "https://dashscope.aliyuncs.com/api/v1",
|
|
model: str = "cosyvoice-v3-flash",
|
|
clone_model: str = "voice-enrollment",
|
|
http_client: httpx.Client | None = None,
|
|
audio_url_signer=None,
|
|
) -> CosyVoiceService:
|
|
return CosyVoiceService(
|
|
api_key=api_key,
|
|
base_url=base_url,
|
|
model=model,
|
|
clone_model=clone_model,
|
|
http_client=http_client,
|
|
audio_url_signer=audio_url_signer,
|
|
)
|
|
|
|
|
|
def _mock_response(
|
|
status_code: int = 200,
|
|
json_data: dict | None = None,
|
|
text: str = "",
|
|
method: str = "POST",
|
|
) -> httpx.Response:
|
|
content = b""
|
|
if json_data is not None:
|
|
content = json.dumps(json_data).encode("utf-8")
|
|
elif text:
|
|
content = text.encode("utf-8")
|
|
|
|
return httpx.Response(
|
|
status_code=status_code,
|
|
content=content,
|
|
request=httpx.Request(method, "https://dashscope.aliyuncs.com/api/v1/test"),
|
|
)
|
|
|
|
|
|
# ── list_preset_voices ───────────────────────────────────
|
|
|
|
|
|
class TestListPresetVoices:
|
|
def test_returns_preset_voices(self) -> None:
|
|
service = _make_service()
|
|
voices = service.list_preset_voices()
|
|
assert len(voices) >= 1
|
|
assert all(isinstance(v, PresetVoice) for v in voices)
|
|
|
|
|
|
# ── submit_clone_task ────────────────────────────────────
|
|
|
|
|
|
class TestSubmitCloneTask:
|
|
def test_submit_success_returns_voice_id(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200,
|
|
{
|
|
"output": {
|
|
"voice_id": "cosyvoice-v3-flash-clone-abc123",
|
|
"status": "DEPLOYING",
|
|
},
|
|
"usage": {"count": 1},
|
|
"request_id": "req-001",
|
|
},
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
result = service.submit_clone_task(
|
|
audio_url="https://example.com/audio.wav",
|
|
voice_name="myvoice",
|
|
)
|
|
|
|
assert result["voice_id"] == "cosyvoice-v3-flash-clone-abc123"
|
|
assert result["status"] == "DEPLOYING"
|
|
assert result["request_id"] == "req-001"
|
|
|
|
# 验证请求参数
|
|
call_args = mock_client.request.call_args
|
|
assert call_args.kwargs["method"] == "POST"
|
|
assert "/services/audio/tts/customization" in call_args.kwargs["url"]
|
|
|
|
payload = call_args.kwargs["json"]
|
|
assert payload["model"] == "voice-enrollment"
|
|
assert payload["input"]["action"] == "create_voice"
|
|
assert payload["input"]["target_model"] == "cosyvoice-v3-flash"
|
|
assert payload["input"]["prefix"] == "myvoice"
|
|
assert payload["input"]["url"] == "https://example.com/audio.wav"
|
|
assert payload["input"]["language_hints"] == ["zh"]
|
|
|
|
def test_submit_with_custom_target_model(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200,
|
|
{
|
|
"output": {
|
|
"voice_id": "cosyvoice-v3-flash-clone-xyz",
|
|
"status": "DEPLOYING",
|
|
},
|
|
"request_id": "req-002",
|
|
},
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
result = service.submit_clone_task(
|
|
audio_url="https://example.com/audio.wav",
|
|
target_model="cosyvoice-v3-flash",
|
|
)
|
|
|
|
payload = mock_client.request.call_args.kwargs["json"]
|
|
assert payload["input"]["target_model"] == "cosyvoice-v3-flash"
|
|
assert result["voice_id"] == "cosyvoice-v3-flash-clone-xyz"
|
|
|
|
def test_submit_empty_audio_url_raises(self) -> None:
|
|
service = _make_service()
|
|
with pytest.raises(ValueError, match="audio_url 不能为空"):
|
|
service.submit_clone_task(audio_url="")
|
|
|
|
def test_submit_no_api_key_raises_auth_error(self) -> None:
|
|
service = _make_service(api_key="")
|
|
with pytest.raises(CosyVoiceAuthError, match="API Key 未配置"):
|
|
service.submit_clone_task(audio_url="https://example.com/a.wav")
|
|
|
|
def test_submit_no_voice_id_raises(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(200, {"output": {}})
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
with pytest.raises(CosyVoiceError, match="未返回 voice_id"):
|
|
service.submit_clone_task(audio_url="https://example.com/a.wav")
|
|
|
|
def test_submit_with_audio_url_signer(self) -> None:
|
|
"""传入 audio_url_signer 时,提交前会对音频URL预签名."""
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200,
|
|
{"output": {"voice_id": "v123", "status": "DEPLOYING"}, "request_id": "r1"},
|
|
)
|
|
|
|
def signer(url: str) -> str:
|
|
return f"{url}?signature=test"
|
|
|
|
service = _make_service(http_client=mock_client, audio_url_signer=signer)
|
|
service.submit_clone_task(audio_url="https://oss.example.com/audio.wav")
|
|
|
|
payload = mock_client.request.call_args.kwargs["json"]
|
|
assert payload["input"]["url"] == "https://oss.example.com/audio.wav?signature=test"
|
|
|
|
def test_submit_signer_failure_falls_back_to_original(self) -> None:
|
|
"""signer 失败时回退到原始URL,不崩溃."""
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200,
|
|
{"output": {"voice_id": "v123", "status": "DEPLOYING"}, "request_id": "r1"},
|
|
)
|
|
|
|
def failing_signer(url: str) -> str:
|
|
raise RuntimeError("sign failed")
|
|
|
|
service = _make_service(http_client=mock_client, audio_url_signer=failing_signer)
|
|
result = service.submit_clone_task(audio_url="https://example.com/a.wav")
|
|
|
|
assert result["voice_id"] == "v123"
|
|
payload = mock_client.request.call_args.kwargs["json"]
|
|
assert payload["input"]["url"] == "https://example.com/a.wav" # 原始URL
|
|
|
|
def test_submit_prefix_sanitized(self) -> None:
|
|
"""voice_name 含特殊字符时清洗为合法 prefix."""
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200, {"output": {"voice_id": "v1", "status": "DEPLOYING"}, "request_id": "r1"}
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
service.submit_clone_task(audio_url="https://e.com/a.wav", voice_name="我的音色-2024!")
|
|
|
|
payload = mock_client.request.call_args.kwargs["json"]
|
|
# 中文和特殊字符被过滤,剩下字母数字
|
|
assert (
|
|
payload["input"]["prefix"] == "2024"
|
|
or payload["input"]["prefix"] == "clone"
|
|
or len(payload["input"]["prefix"]) <= 10
|
|
)
|
|
|
|
def test_submit_auth_401_raises(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(401, text="Unauthorized")
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
with pytest.raises(CosyVoiceAuthError, match="认证失败"):
|
|
service.submit_clone_task(audio_url="https://e.com/a.wav")
|
|
|
|
def test_submit_400_raises_with_code_message(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
400, {"code": "InvalidParameter", "message": "task can not be null"}
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
with pytest.raises(CosyVoiceError, match="InvalidParameter"):
|
|
service.submit_clone_task(audio_url="https://e.com/a.wav")
|
|
|
|
|
|
# ── query_voice_status ──────────────────────────────────
|
|
|
|
|
|
class TestQueryVoiceStatus:
|
|
def test_query_deploying_status(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200,
|
|
{
|
|
"output": {
|
|
"status": "DEPLOYING",
|
|
"target_model": "cosyvoice-v3-flash",
|
|
"gmt_create": "2026-01-01T00:00:00Z",
|
|
"gmt_modified": "2026-01-01T00:01:00Z",
|
|
"resource_link": "https://...",
|
|
},
|
|
"request_id": "req-003",
|
|
},
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
result = service.query_voice_status("voice-123")
|
|
|
|
assert result["status"] == "DEPLOYING"
|
|
assert result["target_model"] == "cosyvoice-v3-flash"
|
|
|
|
# 验证请求
|
|
payload = mock_client.request.call_args.kwargs["json"]
|
|
assert payload["model"] == "voice-enrollment"
|
|
assert payload["input"]["action"] == "query_voice"
|
|
assert payload["input"]["voice_id"] == "voice-123"
|
|
|
|
def test_query_ok_status(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200, {"output": {"status": "OK", "target_model": "cosyvoice-v3-flash"}}
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
result = service.query_voice_status("voice-123")
|
|
assert result["status"] == "OK"
|
|
|
|
def test_query_no_api_key_raises(self) -> None:
|
|
service = _make_service(api_key="")
|
|
with pytest.raises(CosyVoiceAuthError):
|
|
service.query_voice_status("v1")
|
|
|
|
def test_query_empty_voice_id_raises(self) -> None:
|
|
service = _make_service()
|
|
with pytest.raises(ValueError):
|
|
service.query_voice_status("")
|
|
|
|
|
|
# ── poll_clone_task ─────────────────────────────────────
|
|
|
|
|
|
class TestPollCloneTask:
|
|
def test_poll_ok_on_first_check(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200, {"output": {"status": "OK", "target_model": "cosyvoice-v3-flash"}}
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
# 减少轮询间隔加速测试
|
|
service.CLONE_POLL_INTERVAL = 0.01
|
|
|
|
result = service.poll_clone_task("voice-123", timeout=30.0)
|
|
assert result["voice_id"] == "voice-123"
|
|
|
|
def test_poll_deploying_then_ok(self) -> None:
|
|
mock_client = MagicMock()
|
|
# 第一次 DEPLOYING,第二次 OK
|
|
mock_client.request.side_effect = [
|
|
_mock_response(200, {"output": {"status": "DEPLOYING"}}),
|
|
_mock_response(200, {"output": {"status": "OK"}}),
|
|
]
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
service.CLONE_POLL_INTERVAL = 0.01
|
|
|
|
result = service.poll_clone_task("voice-123", timeout=30.0)
|
|
assert result["voice_id"] == "voice-123"
|
|
assert mock_client.request.call_count == 2
|
|
|
|
def test_poll_undeployed_raises_error(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(200, {"output": {"status": "UNDEPLOYED"}})
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
service.CLONE_POLL_INTERVAL = 0.01
|
|
|
|
with pytest.raises(CosyVoiceError, match="审核未通过"):
|
|
service.poll_clone_task("voice-123", timeout=30.0)
|
|
|
|
def test_poll_timeout_raises(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(200, {"output": {"status": "DEPLOYING"}})
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
service.CLONE_POLL_INTERVAL = 0.01
|
|
service.CLONE_MAX_POLL_ATTEMPTS = 3 # 快速失败
|
|
|
|
with pytest.raises(CosyVoiceTimeoutError):
|
|
service.poll_clone_task("voice-123", timeout=30.0)
|
|
|
|
|
|
# ── clone_voice (阻塞) ──────────────────────────────────
|
|
|
|
|
|
class TestCloneVoice:
|
|
def test_clone_already_ok_returns_immediately(self) -> None:
|
|
mock_client = MagicMock()
|
|
# submit 直接返回 OK 状态
|
|
mock_client.request.return_value = _mock_response(
|
|
200,
|
|
{
|
|
"output": {"voice_id": "voice-ok", "status": "OK"},
|
|
"request_id": "req-ok",
|
|
},
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
result = service.clone_voice(audio_url="https://e.com/a.wav")
|
|
|
|
assert isinstance(result, CloneResult)
|
|
assert result.voice_id == "voice-ok"
|
|
assert result.request_id == "req-ok"
|
|
# 只有一次调用(submit),没有 poll
|
|
assert mock_client.request.call_count == 1
|
|
|
|
def test_clone_deploying_then_poll_ok(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.side_effect = [
|
|
# submit: 返回 DEPLOYING
|
|
_mock_response(200, {"output": {"voice_id": "v1", "status": "DEPLOYING"}, "request_id": "r1"}),
|
|
# poll 1: DEPLOYING
|
|
_mock_response(200, {"output": {"status": "DEPLOYING"}}),
|
|
# poll 2: OK
|
|
_mock_response(200, {"output": {"status": "OK"}}),
|
|
]
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
service.CLONE_POLL_INTERVAL = 0.01
|
|
|
|
result = service.clone_voice(audio_url="https://e.com/a.wav")
|
|
assert isinstance(result, CloneResult)
|
|
assert result.voice_id == "v1"
|
|
assert mock_client.request.call_count == 3
|
|
|
|
|
|
# ── synthesize_speech ────────────────────────────────────
|
|
|
|
|
|
class TestSynthesizeSpeech:
|
|
def test_synthesize_success_returns_audio_url(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200,
|
|
{
|
|
"output": {
|
|
"finish_reason": "stop",
|
|
"audio": {
|
|
"url": "https://dashscope-result.oss.com/output.mp3",
|
|
"id": "audio-001",
|
|
"expires_at": 1234567890,
|
|
},
|
|
},
|
|
"usage": {"characters": 10},
|
|
"request_id": "req-syn-001",
|
|
},
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
result = service.synthesize_speech(text="你好世界", voice_id="longxiaochun_v3")
|
|
|
|
assert isinstance(result, SynthesizeResult)
|
|
assert result.audio_url == "https://dashscope-result.oss.com/output.mp3"
|
|
assert result.request_id == "req-syn-001"
|
|
|
|
# 验证请求
|
|
call_args = mock_client.request.call_args
|
|
assert "/services/audio/tts/SpeechSynthesizer" in call_args.kwargs["url"]
|
|
|
|
payload = call_args.kwargs["json"]
|
|
assert payload["model"] == "cosyvoice-v3-flash"
|
|
assert payload["input"]["text"] == "你好世界"
|
|
assert payload["input"]["voice"] == "longxiaochun_v3"
|
|
assert payload["input"]["format"] == "mp3"
|
|
assert payload["input"]["sample_rate"] == 22050
|
|
assert payload["input"]["rate"] == 1.0
|
|
assert payload["input"]["volume"] == 50
|
|
|
|
def test_synthesize_with_custom_params(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200,
|
|
{"output": {"audio": {"url": "https://e.com/out.wav", "id": "a1"}}},
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
service.synthesize_speech(
|
|
text="test",
|
|
voice_id="v1",
|
|
sample_rate=44100,
|
|
format="wav",
|
|
speed=1.5,
|
|
volume=80,
|
|
)
|
|
|
|
payload = mock_client.request.call_args.kwargs["json"]
|
|
assert payload["input"]["sample_rate"] == 44100
|
|
assert payload["input"]["format"] == "wav"
|
|
assert payload["input"]["rate"] == 1.5
|
|
assert payload["input"]["volume"] == 80
|
|
|
|
def test_synthesize_empty_text_raises(self) -> None:
|
|
service = _make_service()
|
|
with pytest.raises(ValueError, match="text 不能为空"):
|
|
service.synthesize_speech(text="", voice_id="v1")
|
|
|
|
def test_synthesize_empty_voice_raises(self) -> None:
|
|
service = _make_service()
|
|
with pytest.raises(ValueError, match="voice_id 不能为空"):
|
|
service.synthesize_speech(text="hi", voice_id="")
|
|
|
|
def test_synthesize_no_api_key_raises(self) -> None:
|
|
service = _make_service(api_key="")
|
|
with pytest.raises(CosyVoiceAuthError):
|
|
service.synthesize_speech(text="hi", voice_id="v1")
|
|
|
|
def test_synthesize_no_audio_url_raises(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(200, {"output": {}})
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
with pytest.raises(CosyVoiceError, match="未返回 audio_url"):
|
|
service.synthesize_speech(text="hi", voice_id="v1")
|
|
|
|
def test_synthesize_submit_returns_empty_task_id(self) -> None:
|
|
"""同步接口的 submit_synthesize_task 返回空 task_id 字段(兼容旧接口)."""
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(
|
|
200,
|
|
{"output": {"audio": {"url": "https://e.com/a.mp3"}}},
|
|
)
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
result = service.submit_synthesize_task(text="hi", voice_id="v1")
|
|
|
|
assert result["task_id"] == "" # 同步接口无 task_id
|
|
assert result["audio_url"] == "https://e.com/a.mp3"
|
|
|
|
def test_synthesize_auth_401_raises(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(401, text="Unauthorized")
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
with pytest.raises(CosyVoiceAuthError):
|
|
service.synthesize_speech(text="hi", voice_id="v1")
|
|
|
|
|
|
# ── retry logic ──────────────────────────────────────────
|
|
|
|
|
|
class TestRetryLogic:
|
|
def test_500_error_retries_then_succeeds(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.side_effect = [
|
|
_mock_response(500, text="Server Error"),
|
|
_mock_response(502, text="Bad Gateway"),
|
|
_mock_response(200, {"output": {"audio": {"url": "https://e.com/a.mp3"}}}),
|
|
]
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
service.RETRY_BACKOFF = 0.01 # 加速
|
|
|
|
result = service.synthesize_speech(text="hi", voice_id="v1")
|
|
assert result.audio_url == "https://e.com/a.mp3"
|
|
assert mock_client.request.call_count == 3
|
|
|
|
def test_max_retries_exhausted_raises(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(500, text="Server Error")
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
service.MAX_RETRIES = 2
|
|
service.RETRY_BACKOFF = 0.01
|
|
|
|
with pytest.raises(CosyVoiceError, match="服务端错误"):
|
|
service.synthesize_speech(text="hi", voice_id="v1")
|
|
|
|
assert mock_client.request.call_count == 2
|
|
|
|
|
|
# ── sanitize_prefix ─────────────────────────────────────
|
|
|
|
|
|
class TestSanitizePrefix:
|
|
def test_alphanumeric_kept(self) -> None:
|
|
service = _make_service()
|
|
assert service._sanitize_prefix("myvoice123") == "myvoice123"
|
|
|
|
def test_special_chars_removed(self) -> None:
|
|
service = _make_service()
|
|
result = service._sanitize_prefix("my-voice_2!")
|
|
# 特殊字符被移除,只保留字母数字
|
|
assert result == "myvoice2"
|
|
|
|
def test_max_10_chars(self) -> None:
|
|
service = _make_service()
|
|
result = service._sanitize_prefix("abcdefghijklmnop")
|
|
assert len(result) == 10
|
|
|
|
def test_empty_returns_clone(self) -> None:
|
|
service = _make_service()
|
|
assert service._sanitize_prefix("!!!???") == "clone"
|
|
assert service._sanitize_prefix("") == "clone"
|
|
|
|
|
|
# ── check_task_status (兼容旧接口) ──────────────────────
|
|
|
|
|
|
class TestCheckTaskStatus:
|
|
def test_check_task_status_uses_query_voice(self) -> None:
|
|
mock_client = MagicMock()
|
|
mock_client.request.return_value = _mock_response(200, {"output": {"status": "OK"}})
|
|
|
|
service = _make_service(http_client=mock_client)
|
|
result = service.check_task_status("voice-123")
|
|
|
|
assert result["status"] == "OK"
|
|
assert result["voice_id"] == "voice-123"
|
|
|
|
# 验证走的是 query_voice 路径
|
|
payload = mock_client.request.call_args.kwargs["json"]
|
|
assert payload["input"]["action"] == "query_voice"
|
|
|
|
|
|
# ── 配置与初始化 ─────────────────────────────────────────
|
|
|
|
|
|
class TestServiceConfiguration:
|
|
"""测试 CosyVoiceService 配置与初始化逻辑."""
|
|
|
|
def test_base_url_old_text2audio_path_auto_fixed(self) -> None:
|
|
"""旧版 base_url 包含 text2audio 路径时,应自动修正为 /api/v1."""
|
|
service = CosyVoiceService(
|
|
api_key="test-key",
|
|
base_url="https://dashscope.aliyuncs.com/api/v1/services/aigc/text2audio",
|
|
model="cosyvoice-v3-flash",
|
|
)
|
|
# 应自动去掉 text2audio 后缀,保留到 /api/v1
|
|
assert service._base_url == "https://dashscope.aliyuncs.com/api/v1"
|
|
|
|
def test_base_url_normal_unchanged(self) -> None:
|
|
"""正常的 base_url 不应被修改."""
|
|
url = "https://dashscope.aliyuncs.com/api/v1"
|
|
service = CosyVoiceService(
|
|
api_key="test-key",
|
|
base_url=url,
|
|
model="cosyvoice-v3-flash",
|
|
)
|
|
assert service._base_url == url
|
|
|
|
def test_base_url_workspace_domain_unchanged(self) -> None:
|
|
"""工作空间专属域名的 base_url 不应被修改."""
|
|
url = "https://workspace-xxx.cn-beijing.maas.aliyuncs.com/api/v1"
|
|
service = CosyVoiceService(
|
|
api_key="test-key",
|
|
base_url=url,
|
|
model="cosyvoice-v3-flash",
|
|
)
|
|
assert service._base_url == url
|