mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 09:20:22 +08:00
refactor: 统一任务框架 Phase 3 - 用 TaskService/FailoverEngine 替代 FallbackOrchestrator
核心重构:
- 移除 FallbackOrchestrator,用 TaskService + FailoverEngine 替代
- TaskService 作为统一入口,支持 SYNC/ASYNC 两种任务模式
- FailoverEngine 实现候选遍历、重试、故障转移逻辑
- 新增 AttemptFunc/AttemptResult 协议,统一尝试结果表示
功能改进:
- 流式响应首字节探测(30s 超时,空流触发故障转移)
- 流式取消归因优化(区分客户端断连 vs 服务端中断)
- 新增 OpenAI Sora 视频取消路由 POST /v1/videos/{task_id}/cancel
- OpenAI 流式请求自动添加 stream_options.include_usage
代码规范:
- 修复 loguru 日志格式(%s → {})
- 新增 FORMAT_CONVERSION_ENABLED 环境变量说明
测试覆盖:
- test_failover_engine.py: FailoverEngine 单元测试
- test_task_service_async_execute.py: TaskService ASYNC 模式测试
- test_video_cancel_e2e.py: 视频取消端到端测试
This commit is contained in:
74
tests/services/test_task_service_async_execute.py
Normal file
74
tests/services/test_task_service_async_execute.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.candidate.schema import CandidateKey
|
||||
from src.services.candidate.submit import SubmitOutcome
|
||||
from src.services.task.context import TaskMode
|
||||
from src.services.task.protocol import AttemptKind
|
||||
from src.services.task.service import TaskService
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_task_service_execute_async_requires_extract_external_task_id() -> None:
|
||||
svc = TaskService(MagicMock())
|
||||
with pytest.raises(ValueError):
|
||||
await svc.execute(
|
||||
task_type="video",
|
||||
task_mode=TaskMode.ASYNC,
|
||||
api_format="openai:video",
|
||||
model_name="m",
|
||||
user_api_key=MagicMock(id="u", user_id="user"),
|
||||
request_func=AsyncMock(),
|
||||
request_id="rid",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_task_service_execute_async_returns_execution_result() -> None:
|
||||
db = MagicMock()
|
||||
svc = TaskService(db)
|
||||
|
||||
candidate = SimpleNamespace(
|
||||
provider=SimpleNamespace(id="p1", name="prov"),
|
||||
endpoint=SimpleNamespace(id="e1"),
|
||||
key=SimpleNamespace(id="k1"),
|
||||
)
|
||||
outcome = SubmitOutcome(
|
||||
candidate=candidate,
|
||||
candidate_keys=[{"index": 0, "provider_id": "p1"}],
|
||||
external_task_id="task_123",
|
||||
rule_lookup=None,
|
||||
upstream_payload={"id": "x"},
|
||||
upstream_headers={"x-test": "1"},
|
||||
upstream_status_code=200,
|
||||
)
|
||||
|
||||
svc.submit_with_failover = AsyncMock(return_value=outcome) # type: ignore[method-assign]
|
||||
svc._recorder.get_candidate_keys = MagicMock( # type: ignore[attr-defined]
|
||||
return_value=[
|
||||
CandidateKey(candidate_index=0, retry_index=0, status="success", provider_id="p1")
|
||||
]
|
||||
)
|
||||
|
||||
result = await svc.execute(
|
||||
task_type="video",
|
||||
task_mode=TaskMode.ASYNC,
|
||||
api_format="openai:video",
|
||||
model_name="m",
|
||||
user_api_key=MagicMock(id="u", user_id="user"),
|
||||
request_func=AsyncMock(),
|
||||
request_id="rid",
|
||||
extract_external_task_id=MagicMock(),
|
||||
allow_format_conversion=True,
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert result.attempt_result is not None
|
||||
assert result.attempt_result.kind == AttemptKind.ASYNC_SUBMIT
|
||||
assert result.provider_task_id == "task_123"
|
||||
assert result.provider_id == "p1"
|
||||
assert result.candidate_index == 0
|
||||
Reference in New Issue
Block a user