feat(plugins): isolate device integrations
This commit is contained in:
+63
-26
@@ -8,7 +8,12 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.mqtt.capture import FRAME_HEADER, RAW_MAGIC, iter_capture_frames
|
||||
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
|
||||
from k1link.device_plugins.xgrids_k1.mqtt.capture import (
|
||||
FRAME_HEADER,
|
||||
RAW_MAGIC,
|
||||
iter_capture_frames,
|
||||
)
|
||||
from k1link.sessions import (
|
||||
LayoutConflictError,
|
||||
SessionIntegrityError,
|
||||
@@ -115,10 +120,10 @@ def test_catalog_reconciles_sessions_removed_from_one_evidence_root(tmp_path: Pa
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
assert store.import_legacy_viewer_live(sessions) == (session.name,)
|
||||
assert store.reconcile_archive(xgrids_k1_archive_source(sessions)) == (session.name,)
|
||||
shutil.rmtree(session)
|
||||
|
||||
assert store.import_legacy_viewer_live(sessions) == ()
|
||||
assert store.reconcile_archive(xgrids_k1_archive_source(sessions)) == ()
|
||||
assert store.list_recent().items == ()
|
||||
|
||||
|
||||
@@ -207,8 +212,8 @@ def test_store_uses_private_wal_database_and_idempotently_imports_legacy_session
|
||||
make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
first = store.import_legacy_viewer_live(sessions)
|
||||
second = store.import_legacy_viewer_live(sessions)
|
||||
first = store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
second = store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
assert first == second == ("20260716T205632Z_viewer_live",)
|
||||
page = store.list_recent()
|
||||
@@ -229,7 +234,7 @@ def test_store_uses_private_wal_database_and_idempotently_imports_legacy_session
|
||||
assert all(source.seekable for source in detail.sources)
|
||||
assert str(repository) not in serialized(detail.as_dict())
|
||||
command = store.prepare_replay(summary.session_id, speed=2.0, loop=True)
|
||||
assert command.source_path.name == "mqtt.raw.k1mqtt"
|
||||
assert command.primary_artifact.path.name == "mqtt.raw.k1mqtt"
|
||||
assert command.speed == 2.0
|
||||
assert command.loop is True
|
||||
|
||||
@@ -238,6 +243,35 @@ def test_store_uses_private_wal_database_and_idempotently_imports_legacy_session
|
||||
assert store.database_path.stat().st_mode & 0o777 == 0o600
|
||||
|
||||
|
||||
def test_reconcile_claims_pre_plugin_catalog_row_without_changing_session_identity(
|
||||
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")
|
||||
source = xgrids_k1_archive_source(sessions)
|
||||
store.reconcile_archive(source)
|
||||
with sqlite3.connect(store.database_path) as connection:
|
||||
connection.execute(
|
||||
"UPDATE observation_sessions SET plugin_id = '', archive_id = '', "
|
||||
"primary_replay_artifact_id = NULL, timeline_origin_epoch_ns = NULL, "
|
||||
"timeline_origin_monotonic_ns = NULL WHERE session_id = ?",
|
||||
(session.name,),
|
||||
)
|
||||
connection.execute(
|
||||
"UPDATE observation_session_artifacts SET replay_byte_length = 0 "
|
||||
"WHERE session_id = ?",
|
||||
(session.name,),
|
||||
)
|
||||
connection.commit()
|
||||
|
||||
assert store.reconcile_archive(source) == (session.name,)
|
||||
command = store.prepare_replay(session.name)
|
||||
assert command.plugin_id == source.plugin_id
|
||||
assert command.primary_artifact.replay_byte_length > 0
|
||||
|
||||
|
||||
def test_recent_sessions_are_sorted_and_cursor_paginated(tmp_path: Path) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
sessions = repository / "sessions"
|
||||
@@ -252,7 +286,7 @@ def test_recent_sessions_are_sorted_and_cursor_paginated(tmp_path: Path) -> None
|
||||
created_at="2026-07-16T20:56:32.699Z",
|
||||
)
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
first = store.list_recent(limit=1)
|
||||
assert first.items[0].session_id == "20260716T205632Z_viewer_live"
|
||||
@@ -271,7 +305,7 @@ def test_legacy_import_adds_video_only_for_validated_recording_tree(tmp_path: Pa
|
||||
make_recorded_camera_source(incomplete, complete=False)
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
complete_detail = store.get_session(complete.name)
|
||||
assert complete_detail.summary.modalities == ("point-cloud", "trajectory", "video")
|
||||
@@ -305,7 +339,7 @@ def test_interrupted_capture_is_recovered_from_aligned_raw_and_metadata_prefix(
|
||||
replace_summary_with_recovery_metadata(session, corrupt_trailing_line=True)
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
assert detail.summary.status == "interrupted"
|
||||
@@ -315,7 +349,7 @@ def test_interrupted_capture_is_recovered_from_aligned_raw_and_metadata_prefix(
|
||||
assert detail.summary.completed_at_utc == "2026-07-16T20:56:35.199Z"
|
||||
assert detail.summary.duration_seconds == 2.5
|
||||
assert detail.artifacts[0].integrity_status == "validated-prefix"
|
||||
assert store.prepare_replay(session.name).source_path.name == "mqtt.raw.k1mqtt"
|
||||
assert store.prepare_replay(session.name).primary_artifact.path.name == "mqtt.raw.k1mqtt"
|
||||
|
||||
|
||||
def test_catalog_upsert_promotes_recovered_session_after_summary_is_completed(
|
||||
@@ -331,12 +365,12 @@ def test_catalog_upsert_promotes_recovered_session_after_summary_is_completed(
|
||||
replace_summary_with_recovery_metadata(session)
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
assert store.get_session(session.name).summary.status == "interrupted"
|
||||
|
||||
summary_path.write_text(completed_summary, encoding="utf-8")
|
||||
metadata_path.write_text(completed_metadata, encoding="utf-8")
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
promoted = store.get_session(session.name).summary
|
||||
assert promoted.status == "ready"
|
||||
@@ -356,7 +390,7 @@ def test_interrupted_capture_does_not_trust_metadata_outside_session(tmp_path: P
|
||||
metadata.symlink_to(outside)
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
assert detail.summary.status == "failed"
|
||||
@@ -376,7 +410,7 @@ def test_interrupted_capture_rejects_newline_terminated_metadata_corruption(
|
||||
stream.write(b"{corrupt\n")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
assert detail.summary.status == "failed"
|
||||
@@ -388,7 +422,7 @@ def test_replay_resolution_rejects_artifact_symlink_escape(tmp_path: Path) -> No
|
||||
sessions = repository / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
raw = session / "captures" / "mqtt_live" / "mqtt.raw.k1mqtt"
|
||||
outside = tmp_path / "outside.k1mqtt"
|
||||
outside.write_bytes(raw.read_bytes())
|
||||
@@ -411,7 +445,7 @@ def test_completed_capture_is_not_replayable_when_declared_integrity_fails(
|
||||
raw.write_bytes(corrupted)
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
assert detail.summary.status == "failed"
|
||||
@@ -431,7 +465,7 @@ def test_completed_capture_is_not_replayable_when_declared_count_is_wrong(
|
||||
summary_path.write_text(json.dumps(summary), encoding="utf-8")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
assert store.get_session(session.name).summary.replayable is False
|
||||
|
||||
@@ -446,13 +480,13 @@ def test_current_session_marker_keeps_active_capture_out_of_replay(tmp_path: Pat
|
||||
)
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
with pytest.raises(SessionNotFoundError):
|
||||
store.get_session(session.name)
|
||||
|
||||
(sessions / ".current_session").unlink()
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
assert store.get_session(session.name).summary.replayable is True
|
||||
|
||||
|
||||
@@ -469,14 +503,17 @@ def test_interrupted_capture_replays_only_committed_prefix_before_partial_raw_ta
|
||||
stream.write(FRAME_HEADER.pack(12, 100)[:7])
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
command = store.prepare_replay(session.name)
|
||||
assert detail.summary.status == "interrupted"
|
||||
assert detail.summary.replayable is True
|
||||
assert command.replay_byte_length == committed_bytes
|
||||
assert command.replay_byte_length < command.source_path.stat().st_size
|
||||
assert command.primary_artifact.replay_byte_length == committed_bytes
|
||||
assert (
|
||||
command.primary_artifact.replay_byte_length
|
||||
< command.primary_artifact.path.stat().st_size
|
||||
)
|
||||
|
||||
|
||||
def test_interrupted_capture_rejects_non_frame_raw_tail(tmp_path: Path) -> None:
|
||||
@@ -489,7 +526,7 @@ def test_interrupted_capture_rejects_non_frame_raw_tail(tmp_path: Path) -> None:
|
||||
stream.write(FRAME_HEADER.pack(0, 0))
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
assert store.get_session(session.name).summary.replayable is False
|
||||
|
||||
@@ -511,11 +548,11 @@ def test_interrupted_capture_tolerates_bounded_group_commit_raw_tail(
|
||||
stream.write(payload)
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
command = store.prepare_replay(session.name)
|
||||
assert store.get_session(session.name).summary.status == "interrupted"
|
||||
assert command.replay_byte_length == committed_bytes
|
||||
assert command.primary_artifact.replay_byte_length == committed_bytes
|
||||
|
||||
|
||||
def test_failed_final_summary_falls_back_to_last_committed_prefix(tmp_path: Path) -> None:
|
||||
@@ -532,7 +569,7 @@ def test_failed_final_summary_falls_back_to_last_committed_prefix(tmp_path: Path
|
||||
summary_path.write_text(json.dumps(summary), encoding="utf-8")
|
||||
store = SessionStore(repository, data_dir=tmp_path / "data")
|
||||
|
||||
store.import_legacy_viewer_live(sessions)
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
assert detail.summary.status == "interrupted"
|
||||
|
||||
Reference in New Issue
Block a user