Files
main_backend/tests/unit/test_project_workspaces_service.py
T
2026-04-27 23:19:04 +03:00

332 lines
11 KiB
Python

from __future__ import annotations
import uuid
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from cpv3.modules.project_workspaces.schemas import (
ActiveJobState,
ProjectWorkspaceState,
SetSourceFileAction,
SilenceSettingsState,
SilenceState,
StartSilenceDetectAction,
StartTranscriptionAction,
TranscriptionRequestState,
)
from cpv3.modules.project_workspaces.service import (
ProjectWorkspaceService,
WorkspaceRevisionConflictError,
)
@pytest.mark.asyncio
async def test_get_workspace_returns_default_state_when_workspace_missing() -> None:
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
service = ProjectWorkspaceService(session=AsyncMock())
service._repo = SimpleNamespace(
get_by_project_id=AsyncMock(return_value=None),
create=AsyncMock(
return_value=SimpleNamespace(
project_id=project.id,
revision=0,
state=ProjectWorkspaceState().model_dump(mode="json"),
)
),
)
workspace = await service.get_workspace(project=project)
assert workspace.revision == 0
assert workspace.phase == "INGEST"
assert workspace.current_screen == "upload"
assert workspace.active_job is None
assert workspace.source_file_id is None
assert workspace.workspace_view.used_file_ids == []
assert workspace.workspace_view.selected_file_id is None
@pytest.mark.asyncio
async def test_apply_action_set_source_file_moves_workspace_to_verify() -> None:
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
requester = SimpleNamespace(id=uuid.uuid4(), is_staff=False)
file_id = uuid.uuid4()
workspace_row = SimpleNamespace(
project_id=project.id,
revision=0,
state=ProjectWorkspaceState().model_dump(mode="json"),
)
saved_state: dict[str, object] = {}
async def update_state(*, project_id, expected_revision, state):
saved_state.update(state)
return SimpleNamespace(
project_id=project_id,
revision=expected_revision + 1,
state=state,
)
service = ProjectWorkspaceService(session=AsyncMock())
service._repo = SimpleNamespace(
get_by_project_id=AsyncMock(return_value=workspace_row),
create=AsyncMock(),
update_state=AsyncMock(side_effect=update_state),
)
service._file_repo = SimpleNamespace(
get_by_id=AsyncMock(
return_value=SimpleNamespace(
id=file_id,
owner_id=requester.id,
project_id=project.id,
path="users/test/source.mp4",
)
)
)
workspace = await service.apply_action(
requester=requester,
project=project,
action=SetSourceFileAction(
type="SET_SOURCE_FILE",
revision=0,
file_id=file_id,
),
)
assert workspace.revision == 1
assert workspace.phase == "VERIFY"
assert workspace.current_screen == "verify"
assert workspace.source_file_id == file_id
assert saved_state["source_file_id"] == str(file_id)
assert saved_state["workspace_view"] == {
"used_file_ids": [str(file_id)],
"selected_file_id": str(file_id),
}
@pytest.mark.asyncio
async def test_apply_action_rejects_stale_revision() -> None:
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
workspace_row = SimpleNamespace(
project_id=project.id,
revision=2,
state=ProjectWorkspaceState().model_dump(mode="json"),
)
service = ProjectWorkspaceService(session=AsyncMock())
service._repo = SimpleNamespace(
get_by_project_id=AsyncMock(return_value=workspace_row),
create=AsyncMock(),
update_state=AsyncMock(),
)
with pytest.raises(WorkspaceRevisionConflictError):
await service.apply_action(
requester=SimpleNamespace(id=uuid.uuid4(), is_staff=False),
project=project,
action=SetSourceFileAction(
type="SET_SOURCE_FILE",
revision=1,
file_id=uuid.uuid4(),
),
)
@pytest.mark.asyncio
async def test_start_silence_detect_submits_task_and_tracks_active_job() -> None:
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
requester = SimpleNamespace(id=uuid.uuid4(), is_staff=False)
source_file_id = uuid.uuid4()
workspace_state = ProjectWorkspaceState(
phase="SILENCE",
source_file_id=source_file_id,
silence=SilenceState(
status="CONFIGURED",
settings=SilenceSettingsState(
min_silence_duration_ms=250,
silence_threshold_db=18,
padding_ms=125,
),
),
)
workspace_row = SimpleNamespace(
project_id=project.id,
revision=0,
state=workspace_state.model_dump(mode="json"),
)
submitted_response = SimpleNamespace(job_id=uuid.uuid4(), status="PENDING")
async def update_state(*, project_id, expected_revision, state):
return SimpleNamespace(
project_id=project_id,
revision=expected_revision + 1,
state=state,
)
task_service = SimpleNamespace(
submit_silence_detect=AsyncMock(return_value=submitted_response),
)
service = ProjectWorkspaceService(session=AsyncMock())
service._repo = SimpleNamespace(
get_by_project_id=AsyncMock(return_value=workspace_row),
create=AsyncMock(),
update_state=AsyncMock(side_effect=update_state),
)
service._file_repo = SimpleNamespace(
get_by_id=AsyncMock(
return_value=SimpleNamespace(
id=source_file_id,
owner_id=requester.id,
project_id=project.id,
path="projects/test/video.mp4",
)
)
)
service._task_service_factory = lambda: task_service
workspace = await service.apply_action(
requester=requester,
project=project,
action=StartSilenceDetectAction(
type="START_SILENCE_DETECT",
revision=0,
),
)
task_service.submit_silence_detect.assert_awaited_once()
assert workspace.current_screen == "processing"
assert workspace.active_job == ActiveJobState(
job_id=submitted_response.job_id,
job_type="SILENCE_DETECT",
)
assert workspace.silence.detect_job_id == submitted_response.job_id
@pytest.mark.asyncio
async def test_start_transcription_persists_request_and_processing_job() -> None:
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
requester = SimpleNamespace(id=uuid.uuid4(), is_staff=False)
source_file_id = uuid.uuid4()
workspace_state = ProjectWorkspaceState(
phase="TRANSCRIPTION",
source_file_id=source_file_id,
)
workspace_row = SimpleNamespace(
project_id=project.id,
revision=3,
state=workspace_state.model_dump(mode="json"),
)
submitted_response = SimpleNamespace(job_id=uuid.uuid4(), status="PENDING")
async def update_state(*, project_id, expected_revision, state):
return SimpleNamespace(
project_id=project_id,
revision=expected_revision + 1,
state=state,
)
task_service = SimpleNamespace(
submit_transcription_generate=AsyncMock(return_value=submitted_response),
)
service = ProjectWorkspaceService(session=AsyncMock())
service._repo = SimpleNamespace(
get_by_project_id=AsyncMock(return_value=workspace_row),
create=AsyncMock(),
update_state=AsyncMock(side_effect=update_state),
)
service._file_repo = SimpleNamespace(
get_by_id=AsyncMock(
return_value=SimpleNamespace(
id=source_file_id,
owner_id=requester.id,
project_id=project.id,
path="projects/test/video.mp4",
)
)
)
service._task_service_factory = lambda: task_service
request = TranscriptionRequestState(engine="whisper", language="ru", model="base")
workspace = await service.apply_action(
requester=requester,
project=project,
action=StartTranscriptionAction(
type="START_TRANSCRIPTION",
revision=3,
request=request,
),
)
assert workspace.current_screen == "transcription-processing"
assert workspace.transcription.request == request
assert workspace.active_job == ActiveJobState(
job_id=submitted_response.job_id,
job_type="TRANSCRIPTION_GENERATE",
)
@pytest.mark.asyncio
async def test_apply_job_update_moves_transcription_job_to_review() -> None:
project = SimpleNamespace(id=uuid.uuid4(), workspace_state=None)
job_id = uuid.uuid4()
transcription_id = uuid.uuid4()
artifact_id = uuid.uuid4()
workspace_state = ProjectWorkspaceState(
phase="TRANSCRIPTION",
active_job=ActiveJobState(job_id=job_id, job_type="TRANSCRIPTION_GENERATE"),
transcription={
"status": "PROCESSING",
"job_id": job_id,
"artifact_id": None,
"transcription_id": None,
"reviewed": False,
},
)
workspace_row = SimpleNamespace(
project_id=project.id,
revision=4,
state=workspace_state.model_dump(mode="json"),
)
async def update_state(*, project_id, expected_revision, state):
return SimpleNamespace(
project_id=project_id,
revision=expected_revision + 1,
state=state,
)
service = ProjectWorkspaceService(session=AsyncMock())
service._repo = SimpleNamespace(
get_by_project_id=AsyncMock(return_value=workspace_row),
create=AsyncMock(),
update_state=AsyncMock(side_effect=update_state),
)
workspace = await service.apply_job_update(
project=project,
job=SimpleNamespace(
id=job_id,
project_id=project.id,
job_type="TRANSCRIPTION_GENERATE",
status="DONE",
output_data={
"transcription_id": str(transcription_id),
"artifact_id": str(artifact_id),
},
),
)
assert workspace is not None
assert workspace.revision == 5
assert workspace.phase == "TRANSCRIPTION"
assert workspace.current_screen == "subtitle-revision"
assert workspace.active_job is None
assert workspace.transcription.transcription_id == transcription_id
assert workspace.transcription.artifact_id == artifact_id
assert workspace.transcription.reviewed is False