fix: finalize recordings before automatic replay

This commit is contained in:
somew1nd committed 2026-08-23 13:06:19 -07:00
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 = [
+3
View File
@@ -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)
+8 -4
View File
@@ -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()
+42
View File
@@ -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()