2026-03-21 12:57:09 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
from unittest.mock import AsyncMock
|
|
|
|
|
|
2026-03-31 19:19:04 +08:00
|
|
|
import httpx
|
2026-03-21 12:57:09 +08:00
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
import src.services.task.video.cancel as cancel_mod
|
2026-04-03 14:59:58 +08:00
|
|
|
import src.services.request.execution_runtime_client as runtime_client_mod
|
2026-03-21 12:57:09 +08:00
|
|
|
from src.core.api_format.conversion.internal_video import VideoStatus
|
2026-04-03 14:59:58 +08:00
|
|
|
from src.services.request.execution_runtime_client import ExecutionRuntimeSyncResult
|
2026-03-21 12:57:09 +08:00
|
|
|
from src.services.task.video.cancel import VideoTaskCancelService
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _Query:
|
|
|
|
|
def __init__(self, result: object) -> None:
|
|
|
|
|
self._result = result
|
|
|
|
|
|
|
|
|
|
def filter(self, *args: object, **kwargs: object) -> "_Query":
|
|
|
|
|
del args, kwargs
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def first(self) -> object:
|
|
|
|
|
return self._result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeDB:
|
|
|
|
|
def __init__(self, *results: object) -> None:
|
|
|
|
|
self._results = list(results)
|
|
|
|
|
self.committed = False
|
|
|
|
|
|
|
|
|
|
def query(self, model: object) -> _Query:
|
|
|
|
|
del model
|
|
|
|
|
return _Query(self._results.pop(0))
|
|
|
|
|
|
|
|
|
|
def commit(self) -> None:
|
|
|
|
|
self.committed = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_video_cancel_service_uses_rust_for_openai_delete(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
endpoint = SimpleNamespace(
|
|
|
|
|
id="ep-1",
|
|
|
|
|
provider_id="prov-1",
|
|
|
|
|
api_family="openai",
|
|
|
|
|
endpoint_kind="video",
|
|
|
|
|
base_url="https://api.openai.com",
|
|
|
|
|
)
|
|
|
|
|
key = SimpleNamespace(id="key-1", api_key="encrypted")
|
|
|
|
|
db = _FakeDB(endpoint, key)
|
|
|
|
|
service = VideoTaskCancelService(db)
|
|
|
|
|
task = SimpleNamespace(
|
|
|
|
|
id="task-1",
|
|
|
|
|
status=VideoStatus.PROCESSING.value,
|
|
|
|
|
external_task_id="ext-1",
|
|
|
|
|
endpoint_id="ep-1",
|
|
|
|
|
key_id="key-1",
|
|
|
|
|
request_id="req-1",
|
|
|
|
|
model="sora-2",
|
|
|
|
|
completed_at=None,
|
|
|
|
|
updated_at=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(cancel_mod.config, "executor_backend", "rust")
|
|
|
|
|
monkeypatch.setattr(
|
2026-04-03 14:59:58 +08:00
|
|
|
runtime_client_mod.ExecutionRuntimeClient,
|
2026-03-21 12:57:09 +08:00
|
|
|
"execute_sync_json",
|
|
|
|
|
AsyncMock(
|
2026-04-03 14:59:58 +08:00
|
|
|
return_value=ExecutionRuntimeSyncResult(
|
2026-03-21 12:57:09 +08:00
|
|
|
status_code=204,
|
|
|
|
|
headers={},
|
|
|
|
|
response_json=None,
|
|
|
|
|
response_body_bytes=None,
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.core.crypto.crypto_service.decrypt",
|
|
|
|
|
lambda value: "upstream-key",
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.core.api_format.build_upstream_headers_for_endpoint",
|
|
|
|
|
lambda *args, **kwargs: {"authorization": "Bearer upstream-key"},
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.services.provider.transport.build_provider_url",
|
|
|
|
|
lambda endpoint, is_stream=False, key=None: "https://api.openai.com/v1/videos",
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.clients.http_client.HTTPClientPool.get_default_client_async",
|
|
|
|
|
AsyncMock(side_effect=AssertionError("python fallback should not run")),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(cancel_mod.UsageService, "finalize_void", lambda *args, **kwargs: True)
|
|
|
|
|
|
|
|
|
|
result = await service.cancel_task(
|
|
|
|
|
task=task,
|
|
|
|
|
task_id="task-1",
|
|
|
|
|
original_headers={"x-test": "1"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result is None
|
|
|
|
|
assert db.committed is True
|
|
|
|
|
assert task.status == VideoStatus.CANCELLED.value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_video_cancel_service_uses_rust_for_gemini_cancel(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
endpoint = SimpleNamespace(
|
|
|
|
|
id="ep-1",
|
|
|
|
|
provider_id="prov-1",
|
|
|
|
|
api_family="gemini",
|
|
|
|
|
endpoint_kind="video",
|
|
|
|
|
base_url="https://generativelanguage.googleapis.com",
|
|
|
|
|
)
|
|
|
|
|
key = SimpleNamespace(id="key-1", api_key="encrypted")
|
|
|
|
|
db = _FakeDB(endpoint, key)
|
|
|
|
|
service = VideoTaskCancelService(db)
|
|
|
|
|
task = SimpleNamespace(
|
|
|
|
|
id="task-1",
|
|
|
|
|
status=VideoStatus.PROCESSING.value,
|
|
|
|
|
external_task_id="operations/ext-1",
|
|
|
|
|
endpoint_id="ep-1",
|
|
|
|
|
key_id="key-1",
|
|
|
|
|
request_id="req-1",
|
|
|
|
|
model="veo-3",
|
|
|
|
|
completed_at=None,
|
|
|
|
|
updated_at=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(cancel_mod.config, "executor_backend", "rust")
|
|
|
|
|
execute_sync = AsyncMock(
|
2026-04-03 14:59:58 +08:00
|
|
|
return_value=ExecutionRuntimeSyncResult(
|
2026-03-21 12:57:09 +08:00
|
|
|
status_code=200,
|
|
|
|
|
headers={"content-type": "application/json"},
|
|
|
|
|
response_json={"ok": True},
|
|
|
|
|
response_body_bytes=None,
|
|
|
|
|
)
|
|
|
|
|
)
|
2026-04-03 14:59:58 +08:00
|
|
|
monkeypatch.setattr(
|
|
|
|
|
runtime_client_mod.ExecutionRuntimeClient,
|
|
|
|
|
"execute_sync_json",
|
|
|
|
|
execute_sync,
|
|
|
|
|
)
|
2026-03-21 12:57:09 +08:00
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.core.crypto.crypto_service.decrypt",
|
|
|
|
|
lambda value: "upstream-key",
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.core.api_format.build_upstream_headers_for_endpoint",
|
|
|
|
|
lambda *args, **kwargs: {"x-goog-api-key": "upstream-key"},
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.services.provider.auth.get_provider_auth",
|
|
|
|
|
AsyncMock(return_value=SimpleNamespace(auth_header="authorization", auth_value="Bearer token")),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.clients.http_client.HTTPClientPool.get_default_client_async",
|
|
|
|
|
AsyncMock(side_effect=AssertionError("python fallback should not run")),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(cancel_mod.UsageService, "finalize_void", lambda *args, **kwargs: True)
|
|
|
|
|
|
|
|
|
|
result = await service.cancel_task(
|
|
|
|
|
task=task,
|
|
|
|
|
task_id="task-1",
|
|
|
|
|
original_headers={"x-test": "1"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result is None
|
|
|
|
|
assert db.committed is True
|
|
|
|
|
assert task.status == VideoStatus.CANCELLED.value
|
|
|
|
|
plan = execute_sync.await_args.args[0]
|
|
|
|
|
assert plan.method == "POST"
|
|
|
|
|
assert plan.body.json_body == {}
|
2026-03-31 19:19:04 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_video_cancel_service_returns_503_when_rust_backend_disabled(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
endpoint = SimpleNamespace(
|
|
|
|
|
id="ep-1",
|
|
|
|
|
provider_id="prov-1",
|
|
|
|
|
api_family="openai",
|
|
|
|
|
endpoint_kind="video",
|
|
|
|
|
base_url="https://api.openai.com",
|
|
|
|
|
)
|
|
|
|
|
key = SimpleNamespace(id="key-1", api_key="encrypted")
|
|
|
|
|
db = _FakeDB(endpoint, key)
|
|
|
|
|
service = VideoTaskCancelService(db)
|
|
|
|
|
task = SimpleNamespace(
|
|
|
|
|
id="task-1",
|
|
|
|
|
status=VideoStatus.PROCESSING.value,
|
|
|
|
|
external_task_id="ext-1",
|
|
|
|
|
endpoint_id="ep-1",
|
|
|
|
|
key_id="key-1",
|
|
|
|
|
request_id="req-1",
|
|
|
|
|
model="sora-2",
|
|
|
|
|
completed_at=None,
|
|
|
|
|
updated_at=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(cancel_mod.config, "executor_backend", "python")
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.core.crypto.crypto_service.decrypt",
|
|
|
|
|
lambda value: "upstream-key",
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.core.api_format.build_upstream_headers_for_endpoint",
|
|
|
|
|
lambda *args, **kwargs: {"authorization": "Bearer upstream-key"},
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.services.provider.transport.build_provider_url",
|
|
|
|
|
lambda endpoint, is_stream=False, key=None: "https://api.openai.com/v1/videos",
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.clients.http_client.HTTPClientPool.get_default_client_async",
|
|
|
|
|
AsyncMock(side_effect=AssertionError("python fallback should not run")),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
response = await service.cancel_task(
|
|
|
|
|
task=task,
|
|
|
|
|
task_id="task-1",
|
|
|
|
|
original_headers={"x-test": "1"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert isinstance(response, httpx.Response)
|
|
|
|
|
assert response.status_code == 503
|
|
|
|
|
assert response.json() == {"error": {"message": "Video 取消仅支持 Rust executor"}}
|
|
|
|
|
assert db.committed is False
|
|
|
|
|
assert task.status == VideoStatus.PROCESSING.value
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
|
|
|
async def test_video_cancel_service_returns_503_when_rust_executor_unavailable(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
endpoint = SimpleNamespace(
|
|
|
|
|
id="ep-1",
|
|
|
|
|
provider_id="prov-1",
|
|
|
|
|
api_family="gemini",
|
|
|
|
|
endpoint_kind="video",
|
|
|
|
|
base_url="https://generativelanguage.googleapis.com",
|
|
|
|
|
)
|
|
|
|
|
key = SimpleNamespace(id="key-1", api_key="encrypted")
|
|
|
|
|
db = _FakeDB(endpoint, key)
|
|
|
|
|
service = VideoTaskCancelService(db)
|
|
|
|
|
task = SimpleNamespace(
|
|
|
|
|
id="task-1",
|
|
|
|
|
status=VideoStatus.PROCESSING.value,
|
|
|
|
|
external_task_id="operations/ext-1",
|
|
|
|
|
endpoint_id="ep-1",
|
|
|
|
|
key_id="key-1",
|
|
|
|
|
request_id="req-1",
|
|
|
|
|
model="veo-3",
|
|
|
|
|
completed_at=None,
|
|
|
|
|
updated_at=None,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(cancel_mod.config, "executor_backend", "rust")
|
|
|
|
|
monkeypatch.setattr(
|
2026-04-03 14:59:58 +08:00
|
|
|
runtime_client_mod.ExecutionRuntimeClient,
|
2026-03-31 19:19:04 +08:00
|
|
|
"execute_sync_json",
|
2026-04-03 14:59:58 +08:00
|
|
|
AsyncMock(side_effect=runtime_client_mod.ExecutionRuntimeClientError("executor down")),
|
2026-03-31 19:19:04 +08:00
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.core.crypto.crypto_service.decrypt",
|
|
|
|
|
lambda value: "upstream-key",
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.core.api_format.build_upstream_headers_for_endpoint",
|
|
|
|
|
lambda *args, **kwargs: {"x-goog-api-key": "upstream-key"},
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.services.provider.auth.get_provider_auth",
|
|
|
|
|
AsyncMock(return_value=None),
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
"src.clients.http_client.HTTPClientPool.get_default_client_async",
|
|
|
|
|
AsyncMock(side_effect=AssertionError("python fallback should not run")),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
response = await service.cancel_task(
|
|
|
|
|
task=task,
|
|
|
|
|
task_id="task-1",
|
|
|
|
|
original_headers={"x-test": "1"},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert isinstance(response, httpx.Response)
|
|
|
|
|
assert response.status_code == 503
|
|
|
|
|
assert response.json() == {"error": {"message": "执行器暂时不可用,请稍后重试"}}
|
|
|
|
|
assert db.committed is False
|
|
|
|
|
assert task.status == VideoStatus.PROCESSING.value
|