feat(archive): complete recorded session lifecycle
This commit is contained in:
@@ -23,6 +23,7 @@ from k1link.sessions import (
|
||||
RecordedMediaInspector,
|
||||
ReplayCommand,
|
||||
SessionIntegrityError,
|
||||
SessionNotFoundError,
|
||||
SessionRecordingMaterializer,
|
||||
SessionRecordingPreparationManager,
|
||||
SessionStore,
|
||||
@@ -246,6 +247,116 @@ def test_session_router_lists_details_and_dispatches_opaque_replay(tmp_path: Pat
|
||||
assert_no_local_paths(value, repository)
|
||||
|
||||
|
||||
def test_delete_session_removes_evidence_and_cache_but_refuses_an_open_recording(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
def export_recording(source: Path, destination: Path) -> dict[str, object]:
|
||||
payload = b"deletable-recording"
|
||||
destination.write_bytes(payload)
|
||||
return {
|
||||
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||
"rrd_sha256": hashlib.sha256(payload).hexdigest(),
|
||||
"rrd_bytes": len(payload),
|
||||
"timeline": "session_time",
|
||||
"timeline_start_ns": 0,
|
||||
"timeline_end_ns": 1,
|
||||
}
|
||||
|
||||
materializer = SessionRecordingMaterializer(
|
||||
store.data_dir,
|
||||
exporter=export_recording,
|
||||
)
|
||||
recording, release = materializer.materialize_pinned(store.prepare_replay(session.name))
|
||||
router = build_session_router(store, recording_materializer=materializer)
|
||||
delete_route = endpoint(router, "/api/v1/observation-sessions/{session_id}", "DELETE")
|
||||
|
||||
with pytest.raises(HTTPException) as conflict:
|
||||
asyncio.run(delete_route(session_id=session.name))
|
||||
assert conflict.value.status_code == 409
|
||||
assert session.is_dir()
|
||||
assert recording.path.is_file()
|
||||
|
||||
release()
|
||||
response = asyncio.run(delete_route(session_id=session.name))
|
||||
|
||||
assert response.status_code == 204
|
||||
assert not session.exists()
|
||||
assert not recording.path.parent.exists()
|
||||
with pytest.raises(SessionNotFoundError):
|
||||
store.get_session(session.name)
|
||||
|
||||
|
||||
def test_completed_recording_get_does_not_hold_delete_for_launch_lease(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
payload = b"recording-opened-and-closed"
|
||||
|
||||
def export_recording(source: Path, destination: Path) -> dict[str, object]:
|
||||
destination.write_bytes(payload)
|
||||
return {
|
||||
"source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
|
||||
"rrd_sha256": hashlib.sha256(payload).hexdigest(),
|
||||
"rrd_bytes": len(payload),
|
||||
"timeline": "session_time",
|
||||
"timeline_start_ns": 0,
|
||||
"timeline_end_ns": 1_000_000_000,
|
||||
}
|
||||
|
||||
materializer = SessionRecordingMaterializer(store.data_dir, exporter=export_recording)
|
||||
command = store.prepare_replay(session.name)
|
||||
materializer.materialize(command)
|
||||
manager = SessionRecordingPreparationManager(materializer)
|
||||
resolved = manager.resolve_cached(command)
|
||||
assert resolved is not None and resolved.recording is not None
|
||||
router = build_session_router(
|
||||
store,
|
||||
recording_materializer=materializer,
|
||||
recording_preparation_manager=manager,
|
||||
)
|
||||
replay_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/replay",
|
||||
"POST",
|
||||
)
|
||||
recording_route = endpoint(
|
||||
router,
|
||||
"/api/v1/observation-sessions/{session_id}/recording.rrd",
|
||||
"GET",
|
||||
)
|
||||
delete_route = endpoint(router, "/api/v1/observation-sessions/{session_id}", "DELETE")
|
||||
try:
|
||||
replay = asyncio.run(replay_route(session_id=session.name, request=None))
|
||||
generation = replay["launch"]["sha256"]
|
||||
file_response = asyncio.run(
|
||||
recording_route(
|
||||
session_id=session.name,
|
||||
generation=generation,
|
||||
if_match=None,
|
||||
if_none_match=None,
|
||||
)
|
||||
)
|
||||
_, body = asyncio.run(render_file_response(file_response, range_header=None))
|
||||
assert body == payload
|
||||
|
||||
deleted = asyncio.run(delete_route(session_id=session.name))
|
||||
|
||||
assert deleted.status_code == 204
|
||||
assert not session.exists()
|
||||
finally:
|
||||
manager.close()
|
||||
|
||||
|
||||
def test_session_router_returns_seekable_recording_and_serves_byte_ranges(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user