chore: claude final touches
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from hmac import new
|
||||
from typing import assert_type
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
|
||||
from cpv3.modules.captions import service as captions_service
|
||||
from cpv3.modules.tasks import service as task_service
|
||||
|
||||
|
||||
def _make_document_json() -> dict:
|
||||
return {
|
||||
"segments": [
|
||||
{
|
||||
"text": "Привет",
|
||||
"semantic_tags": [],
|
||||
"structure_tags": [],
|
||||
"time": {"start": 0.0, "end": 1.0},
|
||||
"lines": [
|
||||
{
|
||||
"text": "Привет",
|
||||
"semantic_tags": [],
|
||||
"structure_tags": [],
|
||||
"time": {"start": 0.0, "end": 1.0},
|
||||
"words": [
|
||||
{
|
||||
"text": "Привет",
|
||||
"semantic_tags": [],
|
||||
"structure_tags": [],
|
||||
"time": {"start": 0.0, "end": 1.0},
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, payload: dict) -> None:
|
||||
self._payload = payload
|
||||
|
||||
def raise_for_status(self) -> None:
|
||||
return None
|
||||
|
||||
def json(self) -> dict:
|
||||
return self._payload
|
||||
|
||||
|
||||
def test_captions_generate_actor_sends_fallback_done_when_callback_not_confirmed(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
sent_events: list[task_service.TaskWebhookEvent] = []
|
||||
|
||||
async def fake_generate_captions(**_: object) -> str:
|
||||
return "render-123"
|
||||
|
||||
monkeypatch.setattr(captions_service, "generate_captions", fake_generate_captions)
|
||||
monkeypatch.setattr(task_service, "_run_async", asyncio.run)
|
||||
monkeypatch.setattr(task_service, "_raise_if_job_cancelled", lambda _job_id: None)
|
||||
monkeypatch.setattr(
|
||||
task_service,
|
||||
"_send_webhook_event",
|
||||
lambda _url, event: sent_events.append(event),
|
||||
)
|
||||
monkeypatch.setattr(task_service.time, "sleep", lambda _seconds: None)
|
||||
monkeypatch.setattr(
|
||||
task_service,
|
||||
"get_settings",
|
||||
lambda: SimpleNamespace(remotion_service_url="http://remotion.test"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
task_service.httpx,
|
||||
"get",
|
||||
lambda *_args, **_kwargs: _FakeResponse(
|
||||
{
|
||||
"status": "done",
|
||||
"output_path": "projects/1/captioned/video.mp4",
|
||||
"callback_delivered": False,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
task_service.captions_generate_actor.fn(
|
||||
job_id=str(uuid.uuid4()),
|
||||
webhook_url="http://backend.test/api/tasks/webhook/job-1/",
|
||||
video_s3_path="uploads/source.mp4",
|
||||
folder="projects/1",
|
||||
transcription_json=_make_document_json(),
|
||||
style_config=None,
|
||||
)
|
||||
|
||||
done_events = [
|
||||
event for event in sent_events if event.status == task_service.JOB_STATUS_DONE
|
||||
]
|
||||
|
||||
assert len(done_events) == 1
|
||||
assert done_events[0].progress_pct == task_service.PROGRESS_COMPLETE
|
||||
assert done_events[0].current_message == "Готово"
|
||||
assert done_events[0].output_data == {
|
||||
"output_path": "projects/1/captioned/video.mp4"
|
||||
}
|
||||
|
||||
|
||||
def test_captions_generate_actor_skips_fallback_when_done_webhook_confirmed(
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
sent_events: list[task_service.TaskWebhookEvent] = []
|
||||
|
||||
async def fake_generate_captions(**_: object) -> str:
|
||||
return "render-123"
|
||||
|
||||
monkeypatch.setattr(captions_service, "generate_captions", fake_generate_captions)
|
||||
monkeypatch.setattr(task_service, "_run_async", asyncio.run)
|
||||
monkeypatch.setattr(task_service, "_raise_if_job_cancelled", lambda _job_id: None)
|
||||
monkeypatch.setattr(
|
||||
task_service,
|
||||
"_send_webhook_event",
|
||||
lambda _url, event: sent_events.append(event),
|
||||
)
|
||||
monkeypatch.setattr(task_service.time, "sleep", lambda _seconds: None)
|
||||
monkeypatch.setattr(
|
||||
task_service,
|
||||
"get_settings",
|
||||
lambda: SimpleNamespace(remotion_service_url="http://remotion.test"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
task_service.httpx,
|
||||
"get",
|
||||
lambda *_args, **_kwargs: _FakeResponse(
|
||||
{
|
||||
"status": "done",
|
||||
"output_path": "projects/1/captioned/video.mp4",
|
||||
"callback_delivered": True,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
task_service.captions_generate_actor.fn(
|
||||
job_id=str(uuid.uuid4()),
|
||||
webhook_url="http://backend.test/api/tasks/webhook/job-1/",
|
||||
video_s3_path="uploads/source.mp4",
|
||||
folder="projects/1",
|
||||
transcription_json=_make_document_json(),
|
||||
style_config=None,
|
||||
)
|
||||
|
||||
done_events = [
|
||||
event for event in sent_events if event.status == task_service.JOB_STATUS_DONE
|
||||
]
|
||||
|
||||
assert done_events == []
|
||||
@@ -0,0 +1,108 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from cpv3.modules.tasks.schemas import CaptionsGenerateRequest, TaskWebhookEvent
|
||||
from cpv3.modules.tasks.service import TaskService
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_submit_captions_generate_reuses_existing_active_job() -> None:
|
||||
service = TaskService(session=AsyncMock())
|
||||
existing_job_id = uuid.uuid4()
|
||||
existing_job = SimpleNamespace(
|
||||
id=existing_job_id,
|
||||
status="RUNNING",
|
||||
)
|
||||
|
||||
service._find_duplicate_active_job = AsyncMock(return_value=existing_job)
|
||||
service._submit_task = AsyncMock()
|
||||
|
||||
response = await service.submit_captions_generate(
|
||||
requester=SimpleNamespace(id=uuid.uuid4()),
|
||||
request=CaptionsGenerateRequest(
|
||||
video_s3_path="projects/test/video.mp4",
|
||||
folder="output_files",
|
||||
transcription_id=uuid.uuid4(),
|
||||
project_id=uuid.uuid4(),
|
||||
preset_id=uuid.uuid4(),
|
||||
),
|
||||
)
|
||||
|
||||
assert response.job_id == existing_job_id
|
||||
assert response.status == "RUNNING"
|
||||
assert response.webhook_url.endswith(f"/api/tasks/webhook/{existing_job_id}/")
|
||||
service._submit_task.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_webhook_event_ignores_cancelled_job() -> None:
|
||||
cancelled_job = SimpleNamespace(
|
||||
id=uuid.uuid4(),
|
||||
status="CANCELLED",
|
||||
)
|
||||
job_repo = SimpleNamespace(
|
||||
get_by_id=AsyncMock(return_value=cancelled_job),
|
||||
update=AsyncMock(),
|
||||
)
|
||||
event_repo = SimpleNamespace(create=AsyncMock())
|
||||
|
||||
service = TaskService(session=AsyncMock())
|
||||
service._job_repo = job_repo
|
||||
service._event_repo = event_repo
|
||||
|
||||
result = await service.record_webhook_event(
|
||||
job_id=cancelled_job.id,
|
||||
event=TaskWebhookEvent(
|
||||
status="DONE",
|
||||
current_message="Готово",
|
||||
output_data={"output_path": "projects/test/output.mp4"},
|
||||
),
|
||||
)
|
||||
|
||||
assert result is cancelled_job
|
||||
job_repo.update.assert_not_awaited()
|
||||
event_repo.create.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_job_marks_job_cancelled_and_keeps_record() -> None:
|
||||
job_id = uuid.uuid4()
|
||||
user_id = uuid.uuid4()
|
||||
job = SimpleNamespace(
|
||||
id=job_id,
|
||||
status="PENDING",
|
||||
broker_id="default:redis-message-id",
|
||||
job_type="CAPTIONS_GENERATE",
|
||||
user_id=user_id,
|
||||
)
|
||||
cancelled_job = SimpleNamespace(
|
||||
id=job_id,
|
||||
status="CANCELLED",
|
||||
broker_id="default:redis-message-id",
|
||||
job_type="CAPTIONS_GENERATE",
|
||||
user_id=user_id,
|
||||
current_message="Отменено пользователем",
|
||||
)
|
||||
|
||||
service = TaskService(session=AsyncMock())
|
||||
service._job_repo = SimpleNamespace(update=AsyncMock(return_value=cancelled_job))
|
||||
service._event_repo = SimpleNamespace(create=AsyncMock())
|
||||
service._cancel_dramatiq_message = AsyncMock()
|
||||
service._cancel_caption_render = AsyncMock()
|
||||
service._create_cancellation_notification = AsyncMock()
|
||||
|
||||
result = await service.cancel_job(job)
|
||||
|
||||
assert result is cancelled_job
|
||||
service._job_repo.update.assert_awaited_once()
|
||||
service._event_repo.create.assert_awaited_once()
|
||||
service._cancel_dramatiq_message.assert_awaited_once_with(job.broker_id)
|
||||
service._cancel_caption_render.assert_awaited_once_with(job)
|
||||
service._create_cancellation_notification.assert_awaited_once_with(
|
||||
cancelled_job
|
||||
)
|
||||
Reference in New Issue
Block a user