fix: finalize recordings before automatic replay
This commit is contained in:
1 parent
474e56ef8e
commit
a62ac4aef1
25 files changed
+820
-92
No files matched your search
@@ -0,0 +1,84 @@
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.admin_console.routers import media as media_router
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_video_does_not_expose_recording_in_progress(monkeypatch):
|
||||
repo = MagicMock()
|
||||
repo.get_video_recordings_map.return_value = {}
|
||||
repo.get_session_by_id.return_value = {"session_id": "session-1", "status": "completed"}
|
||||
repo.get_video_recording_for_session.return_value = {
|
||||
"session_id": "session-1",
|
||||
"status": "recording",
|
||||
"local_video_path": "/tmp/recording.mkv",
|
||||
}
|
||||
monkeypatch.setattr(media_router, "session_repo", repo, raising=False)
|
||||
monkeypatch.setattr(media_router.media_service, "build_video_index", MagicMock(return_value={}))
|
||||
resolve = MagicMock(return_value="/videos/recording.mkv")
|
||||
monkeypatch.setattr(media_router.media_service, "resolve_video_url", resolve)
|
||||
|
||||
response = await media_router.get_session_video("session-1")
|
||||
|
||||
assert response["status"] == "processing"
|
||||
assert response["has_video"] is False
|
||||
assert response["video_url"] is None
|
||||
resolve.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_video_publishes_only_finalized_versioned_media(monkeypatch):
|
||||
repo = MagicMock()
|
||||
repo.get_video_recordings_map.return_value = {"session-1": "/tmp/recording.mp4"}
|
||||
repo.get_session_by_id.return_value = {"session_id": "session-1", "status": "completed"}
|
||||
repo.get_video_recording_for_session.return_value = {
|
||||
"session_id": "session-1",
|
||||
"status": "ready",
|
||||
"end_time": 123.456,
|
||||
"local_video_path": "/tmp/recording.mp4",
|
||||
}
|
||||
monkeypatch.setattr(media_router, "session_repo", repo, raising=False)
|
||||
monkeypatch.setattr(media_router.media_service, "build_video_index", MagicMock(return_value={}))
|
||||
monkeypatch.setattr(
|
||||
media_router.media_service,
|
||||
"resolve_video_url",
|
||||
MagicMock(return_value="/videos/recording.mp4"),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
media_router.media_service,
|
||||
"resolve_video_segments",
|
||||
MagicMock(
|
||||
return_value=[
|
||||
{"url": "/videos/recording.mp4", "start": 0, "duration": 4, "width": 1080, "height": 1920}
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
response = await media_router.get_session_video("session-1")
|
||||
|
||||
assert response["status"] == "ready"
|
||||
assert response["has_video"] is True
|
||||
assert response["video_url"] == "/videos/recording.mp4?v=123456"
|
||||
assert response["video_segments"][0]["url"] == "/videos/recording.mp4?v=123456"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_session_video_surfaces_terminal_recording_failure(monkeypatch):
|
||||
repo = MagicMock()
|
||||
repo.get_video_recordings_map.return_value = {}
|
||||
repo.get_session_by_id.return_value = {"session_id": "session-1", "status": "failed"}
|
||||
repo.get_video_recording_for_session.return_value = {
|
||||
"session_id": "session-1",
|
||||
"status": "failed",
|
||||
"error": "ffmpeg failed",
|
||||
}
|
||||
monkeypatch.setattr(media_router, "session_repo", repo, raising=False)
|
||||
monkeypatch.setattr(media_router.media_service, "build_video_index", MagicMock(return_value={}))
|
||||
|
||||
response = await media_router.get_session_video("session-1")
|
||||
|
||||
assert response["status"] == "failed"
|
||||
assert response["message"] == "ffmpeg failed"
|
||||
assert response["video_url"] is None
|
||||
@@ -71,6 +71,26 @@ async def test_get_next_pending_task():
|
||||
assert next_task["goal"] == "Goal A"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("current_status", "returncode", "stopped", "expected"),
|
||||
[
|
||||
("completed", 1, False, ("completed", False)),
|
||||
("failed", 0, False, ("failed", False)),
|
||||
("cancelled", 0, False, ("cancelled", False)),
|
||||
("success", 1, False, ("completed", True)),
|
||||
("running", 0, False, ("completed", True)),
|
||||
("running", 1, False, ("failed", True)),
|
||||
("completed", 0, True, ("cancelled", True)),
|
||||
],
|
||||
)
|
||||
def test_resolve_terminal_status_preserves_authoritative_result(
|
||||
current_status, returncode, stopped, expected
|
||||
):
|
||||
assert (
|
||||
TaskQueueService._resolve_terminal_status(current_status, returncode, stopped) == expected
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_remove_task():
|
||||
state.queue_items = [
|
||||
|
||||
@@ -191,16 +191,19 @@ def test_video_recording_persistence(tmp_path):
|
||||
assert persisted.start_time == 100.0
|
||||
assert persisted.end_time is None
|
||||
assert persisted.local_video_path == "/tmp/recording.mp4"
|
||||
assert persisted.status == "recording"
|
||||
|
||||
# Test update
|
||||
record.end_time = 150.0
|
||||
record.local_video_path = "/tmp/recording_final.mp4"
|
||||
record.status = "ready"
|
||||
storage.update_video_recording(record)
|
||||
|
||||
updated = storage.get_video_recording(video_id)
|
||||
assert updated is not None
|
||||
assert updated.end_time == 150.0
|
||||
assert updated.local_video_path == "/tmp/recording_final.mp4"
|
||||
assert updated.status == "ready"
|
||||
|
||||
|
||||
def test_record_step_suppresses_identical_post_screenshot(tmp_path):
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from artemis.sdk.agent import Agent
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_trace_finalization_failure_does_not_escape():
|
||||
agent = object.__new__(Agent)
|
||||
agent._finalize_tracing = AsyncMock(side_effect=ValueError("bad recording manifest"))
|
||||
task = MagicMock(status="completed")
|
||||
task.get_name.return_value = "successful-task"
|
||||
context = MagicMock()
|
||||
|
||||
await agent._finalize_tracing_safely(task=task, context=context)
|
||||
|
||||
agent._finalize_tracing.assert_awaited_once_with(task=task, context=context)
|
||||
@@ -243,9 +243,10 @@ async def test_unified_controller_stop_recording(mock_ctx, tmp_path):
|
||||
|
||||
with patch(
|
||||
"artemis.controllers.unified_controller.remux_recording_to_mp4",
|
||||
AsyncMock(side_effect=lambda src, dst: dst.write_bytes(b"mp4 content") or True),
|
||||
AsyncMock(side_effect=lambda src, dst: (dst.write_bytes(b"mp4 content"), True)[1]),
|
||||
), patch(
|
||||
"artemis.controllers.unified_controller.write_recording_manifest", AsyncMock()
|
||||
"artemis.controllers.unified_controller.write_recording_manifest",
|
||||
AsyncMock(return_value=tmp_path / "recording.json"),
|
||||
):
|
||||
res = await controller.stop_video_recording()
|
||||
|
||||
@@ -363,9 +364,12 @@ async def test_unified_controller_crash_recovery_and_multi_segment(mock_ctx, tmp
|
||||
with (
|
||||
patch(
|
||||
"artemis.controllers.unified_controller.remux_recording_to_mp4",
|
||||
AsyncMock(side_effect=lambda src, dst: dst.write_bytes(b"final mp4") or True),
|
||||
AsyncMock(side_effect=lambda src, dst: (dst.write_bytes(b"final mp4"), True)[1]),
|
||||
),
|
||||
patch(
|
||||
"artemis.controllers.unified_controller.write_recording_manifest",
|
||||
AsyncMock(return_value=tmp_path / "recording.json"),
|
||||
),
|
||||
patch("artemis.controllers.unified_controller.write_recording_manifest", AsyncMock()),
|
||||
):
|
||||
res = await controller.stop_video_recording()
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Copyright 2026 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
|
||||
from artemis.utils.media import (
|
||||
create_steps_json_from_trace_folder,
|
||||
remove_steps_json_from_trace_folder,
|
||||
)
|
||||
|
||||
|
||||
def test_step_compilation_preserves_recording_manifest(tmp_path):
|
||||
step_file = tmp_path / "123.json"
|
||||
step_file.write_text('{"action": "click"}', encoding="utf-8")
|
||||
recording_manifest = tmp_path / "recording.json"
|
||||
recording_manifest.write_text(
|
||||
'{"version": 1, "segments": [{"file": "recording.mp4"}]}',
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
create_steps_json_from_trace_folder(tmp_path)
|
||||
|
||||
compiled = json.loads((tmp_path / "steps.json").read_text(encoding="utf-8"))
|
||||
assert compiled == [{"timestamp": 123, "data": '{"action": "click"}'}]
|
||||
assert recording_manifest.exists()
|
||||
|
||||
remove_steps_json_from_trace_folder(tmp_path)
|
||||
|
||||
assert not step_file.exists()
|
||||
assert recording_manifest.exists()
|
||||
assert (tmp_path / "steps.json").exists()
|
||||
Reference in new issue
Block a user