feat(plugins): isolate device integrations

This commit is contained in:
DCCONSTRUCTIONS
2026-07-17 19:29:32 +03:00
parent f9ffb7bd1c
commit 24a47318f2
122 changed files with 3304 additions and 1892 deletions
+1 -1
View File
@@ -2,8 +2,8 @@ from __future__ import annotations
from pathlib import Path
from k1link.device_plugins.xgrids_k1.archive import discover_legacy_viewer_sessions
from k1link.sessions import ActiveSessionLease, recover_stale_active_session_marker
from k1link.sessions.legacy import discover_legacy_viewer_sessions
def test_active_session_lease_hides_live_evidence_until_release(tmp_path: Path) -> None:
+1 -1
View File
@@ -1,7 +1,7 @@
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from k1link.ble.scanner import advertisement_record
from k1link.device_plugins.xgrids_k1.ble.scanner import advertisement_record
def test_advertisement_record_marks_k1_candidate() -> None:
+1 -1
View File
@@ -10,7 +10,7 @@ from pathlib import Path
import pytest
from k1link.sessions.legacy import discover_legacy_viewer_sessions
from k1link.device_plugins.xgrids_k1.archive import discover_legacy_viewer_sessions
from k1link.web.camera_archive import (
CAMERA_ARCHIVE_SCHEMA,
CAMERA_COMMIT_POLICY,
+13 -8
View File
@@ -8,7 +8,7 @@ from pathlib import Path
import lz4.block
import pytest
import k1link.viewer.runtime as runtime_module
import k1link.device_plugins.xgrids_k1.viewer.runtime as runtime_module
from k1link.data_plane import (
ConsumerFrameContext,
DecodedDeviceStatusView,
@@ -16,11 +16,11 @@ from k1link.data_plane import (
DecodedPoseView,
NormalizationError,
)
from k1link.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.protocol.normalizer import normalize_k1_message
from k1link.viewer.messages import StreamMessage
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.device_plugins.xgrids_k1.protocol.normalizer import normalize_k1_message
from k1link.device_plugins.xgrids_k1.viewer.messages import StreamMessage
from k1link.device_plugins.xgrids_k1.viewer.runtime import VisualizationRuntime
from k1link.viewer.rerun_bridge import RerunBridge
from k1link.viewer.runtime import VisualizationRuntime
class FakeRecording:
@@ -232,7 +232,7 @@ def test_canonical_contracts_reject_misaligned_data_and_support_status() -> None
def test_rerun_bridge_source_has_no_vendor_protocol_or_raw_transport_knowledge() -> None:
source = inspect.getsource(__import__("k1link.viewer.rerun_bridge", fromlist=["*"]))
for forbidden in (
"k1link.protocol",
"k1link.device_plugins.xgrids_k1.protocol",
"StreamMessage",
"RealtimePointcloud",
"RealtimePath",
@@ -245,9 +245,14 @@ def test_rerun_bridge_source_has_no_vendor_protocol_or_raw_transport_knowledge()
def test_visual_runtime_has_no_implicit_vendor_normalizer() -> None:
source = inspect.getsource(__import__("k1link.viewer.runtime", fromlist=["*"]))
source = inspect.getsource(
__import__(
"k1link.device_plugins.xgrids_k1.viewer.runtime",
fromlist=["*"],
)
)
assert "k1link.protocol" not in source
assert "k1link.device_plugins.xgrids_k1.protocol" not in source
assert "normalize_k1_message" not in source
assert "normalizer: CanonicalNormalizer" in source
+2 -2
View File
@@ -4,7 +4,7 @@ from typing import Any
from typer.testing import CliRunner
from k1link.cli import app
from k1link.device_plugins.xgrids_k1.cli import app
runner = CliRunner()
@@ -59,7 +59,7 @@ def test_mqtt_capture_cli_uses_bounded_read_only_capture(
"payload_bytes": 128,
}
monkeypatch.setattr("k1link.cli.capture_mqtt", fake_capture)
monkeypatch.setattr("k1link.device_plugins.xgrids_k1.cli.capture_mqtt", fake_capture)
out = tmp_path / "capture"
result = runner.invoke(
app,
@@ -0,0 +1,139 @@
from __future__ import annotations
import hashlib
from pathlib import Path
from k1link.sessions import (
ObservationArchiveSource,
ObservationArtifactCandidate,
ObservationSessionCandidate,
SessionRecordingMaterializer,
SessionStore,
)
from k1link.sessions.models import SessionSource
SYNTHETIC_PLUGIN_ID = "example.synthetic-spatial-sensor"
def test_second_device_can_discover_catalog_and_prepare_without_core_changes(
tmp_path: Path,
) -> None:
archive_root = tmp_path / "synthetic-evidence"
session_root = archive_root / "synthetic-session-001"
session_root.mkdir(parents=True)
native = session_root / "observation.synthetic"
native.write_bytes(b"synthetic vendor-native evidence")
source = ObservationArchiveSource(
plugin_id=SYNTHETIC_PLUGIN_ID,
archive_id="synthetic.archive.v1",
root=archive_root,
discover=lambda root: (_synthetic_candidate(root, session_root, native),),
)
store = SessionStore(tmp_path / "repository", data_dir=tmp_path / "host-data")
assert store.reconcile_archive(source) == ("synthetic-session-001",)
command = store.prepare_replay("synthetic-session-001")
assert command.plugin_id == SYNTHETIC_PLUGIN_ID
assert command.primary_artifact.media_type == "application/x-synthetic-spatial"
materializer = SessionRecordingMaterializer(
store.data_dir,
exporters={SYNTHETIC_PLUGIN_ID: _synthetic_exporter},
)
recording = materializer.materialize(command)
assert recording.path.read_bytes().startswith(b"synthetic-rrd:")
assert recording.timeline == "session_time"
assert recording.timeline_start_ns == 0
assert recording.timeline_end_ns == 1_000_000_000
def test_generic_observation_hot_path_contains_no_concrete_device_vocabulary() -> None:
repository_root = Path(__file__).resolve().parents[1]
generic_paths = (
*(repository_root / "src" / "k1link" / "sessions").glob("*.py"),
repository_root / "src" / "k1link" / "web" / "app.py",
repository_root / "src" / "k1link" / "web" / "session_api.py",
repository_root / "src" / "k1link" / "web" / "plugin_runtime.py",
repository_root / "src" / "k1link" / "web" / "device_plugin_composition.py",
repository_root / "src" / "k1link" / "viewer" / "recorded.py",
repository_root / "src" / "k1link" / "viewer" / "rerun_bridge.py",
)
forbidden = (
"xgrids",
"k1mqtt",
"realtimepointcloud",
"realtimepath",
"lio_pcl",
"lio_pose",
"mqtt.metadata",
"k1link.device_plugins",
)
violations = {
str(path.relative_to(repository_root)): token
for path in generic_paths
for token in forbidden
if token in path.read_text(encoding="utf-8").casefold()
}
assert violations == {}
def _synthetic_candidate(
archive_root: Path,
session_root: Path,
native: Path,
) -> ObservationSessionCandidate:
artifact_id = "native-primary"
return ObservationSessionCandidate(
session_id="synthetic-session-001",
display_name="Synthetic spatial sensor",
status="ready",
started_at_utc="2026-07-17T10:00:00Z",
completed_at_utc="2026-07-17T10:00:01Z",
duration_seconds=1.0,
modalities=("point-cloud",),
replayable=True,
total_bytes=native.stat().st_size,
allowed_root=archive_root.resolve(),
session_root=session_root.resolve(),
primary_replay_artifact_id=artifact_id,
timeline_origin_epoch_ns=1_700_000_000_000_000_000,
timeline_origin_monotonic_ns=10_000_000,
sources=(
SessionSource(
source_id="spatial.primary",
semantic_channel_id="spatial.point-cloud.recorded",
modality="point-cloud",
status="recorded",
seekable=True,
artifact_id=artifact_id,
),
),
artifacts=(
ObservationArtifactCandidate(
artifact_id=artifact_id,
kind="native-observation",
media_type="application/x-synthetic-spatial",
locator=native.resolve(),
byte_length=native.stat().st_size,
replay_byte_length=native.stat().st_size,
sha256=None,
integrity_status="validated-structure",
),
),
)
def _synthetic_exporter(source: Path, destination: Path) -> dict[str, object]:
payload = b"synthetic-rrd:" + source.read_bytes()
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,
}
+2 -2
View File
@@ -2,14 +2,14 @@ from __future__ import annotations
import struct
from k1link.protocol.streams import (
from k1link.device_plugins.xgrids_k1.protocol.streams import (
LegacyPoint,
LegacyPointCloudFrame,
LioPoint,
LioPointCloudFrame,
MqttHeader,
)
from k1link.viewer.foxglove_bridge import (
from k1link.device_plugins.xgrids_k1.viewer.foxglove_bridge import (
POINT_STRIDE,
BridgeMetrics,
pack_legacy_point_cloud,
+10 -15
View File
@@ -7,7 +7,7 @@ def test_mission_core_frontend_has_single_plugin_composition_root() -> None:
repository_root = Path(__file__).resolve().parents[1]
source_root = repository_root / "apps" / "control-station" / "src"
composition = source_root / "composition" / "devicePlugins.ts"
forbidden_import = "device-plugins/xgrids-k1"
forbidden_import = "@xgrids-k1/frontend"
imports = [
path.relative_to(source_root).as_posix()
@@ -50,9 +50,7 @@ def test_mission_core_shell_does_not_know_xgrids_wire_fields() -> None:
def test_xgrids_client_uses_manifest_identity_and_plugin_scoped_events() -> None:
repository_root = Path(__file__).resolve().parents[1]
plugin_root = (
repository_root / "apps" / "control-station" / "src" / "device-plugins" / "xgrids-k1"
)
plugin_root = repository_root / "plugins" / "xgrids-k1" / "frontend" / "src"
api_source = (plugin_root / "api.ts").read_text("utf-8")
manifest_source = (plugin_root / "manifest.ts").read_text("utf-8")
@@ -74,12 +72,11 @@ def test_backend_host_has_no_concrete_xgrids_imports() -> None:
def test_model_switch_is_guarded_by_plugin_deactivation() -> None:
repository_root = Path(__file__).resolve().parents[1]
frontend_root = repository_root / "apps" / "control-station" / "src"
plugin_root = repository_root / "plugins" / "xgrids-k1" / "frontend" / "src"
host_source = (frontend_root / "core" / "device-plugins" / "DevicePluginHost.tsx").read_text(
"utf-8"
)
xgrids_runtime = (
frontend_root / "device-plugins" / "xgrids-k1" / "runtimeContext.tsx"
).read_text("utf-8")
xgrids_runtime = (plugin_root / "runtimeContext.tsx").read_text("utf-8")
deactivation_guard = "if (!(await deactivate()))"
assert "if (current && !deactivate)" in host_source
@@ -94,9 +91,7 @@ def test_model_switch_is_guarded_by_plugin_deactivation() -> None:
def test_xgrids_frontend_uses_semantic_acquisition_actions_and_stable_identity() -> None:
repository_root = Path(__file__).resolve().parents[1]
plugin_root = (
repository_root / "apps" / "control-station" / "src" / "device-plugins" / "xgrids-k1"
)
plugin_root = repository_root / "plugins" / "xgrids-k1" / "frontend" / "src"
manifest_source = (plugin_root / "manifest.ts").read_text("utf-8")
api_source = (plugin_root / "api.ts").read_text("utf-8")
hook_source = (plugin_root / "useXgridsK1Runtime.ts").read_text("utf-8")
@@ -121,12 +116,12 @@ def test_xgrids_live_copy_does_not_claim_software_controls_the_physical_scanner(
repository_root = Path(__file__).resolve().parents[1]
connection_source = (
repository_root
/ "apps"
/ "control-station"
/ "src"
/ "device-plugins"
/ "plugins"
/ "xgrids-k1"
/ "XgridsK1Connection.tsx"
/ "frontend"
/ "src"
/ "components"
/ "K1AcquisitionPipeline.tsx"
).read_text("utf-8")
assert "Подготовить приём данных" in connection_source
+2 -2
View File
@@ -10,8 +10,8 @@ import pytest
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.reasoncodes import ReasonCode
import k1link.mqtt.capture as capture_module
from k1link.mqtt.capture import (
import k1link.device_plugins.xgrids_k1.mqtt.capture as capture_module
from k1link.device_plugins.xgrids_k1.mqtt.capture import (
FRAME_HEADER,
RAW_MAGIC,
REPORT_TOPICS,
+1 -1
View File
@@ -1,4 +1,4 @@
from k1link.net.snapshot import command_record, snapshot
from k1link.device_plugins.xgrids_k1.net.snapshot import command_record, snapshot
def test_command_record_captures_success() -> None:
+18 -18
View File
@@ -3,27 +3,15 @@ from __future__ import annotations
import asyncio
import json
import threading
from collections.abc import Mapping
from pathlib import Path
from typing import Any
import pytest
from missioncore_plugin_sdk.v0alpha2 import RuntimeActionInvocation
from pydantic import ValidationError
import k1link.web.device_plugin_composition as plugin_composition
from k1link.web.device_plugin_composition import (
DevicePluginCompositionError,
InstalledDevicePluginEnvironment,
load_installed_device_plugins,
)
from k1link.web.plugin_catalog import DevicePluginCatalog
from k1link.web.plugin_runtime import (
DevicePluginDispatcher,
DevicePluginRuntimeContribution,
PluginActionNotFoundError,
PluginNotFoundError,
)
from k1link.web.xgrids_k1_facade import (
from k1link.device_plugins.xgrids_k1.facade import (
ACTION_DISCOVERY_SCAN,
ACTION_NETWORK_PROVISION,
ACTION_STREAM_START_LIVE,
@@ -36,6 +24,18 @@ from k1link.web.xgrids_k1_facade import (
ViewerSettingsRequest,
XgridsK1PluginFacade,
)
from k1link.web.device_plugin_composition import (
DevicePluginCompositionError,
InstalledDevicePluginEnvironment,
load_installed_device_plugins,
)
from k1link.web.plugin_catalog import DevicePluginCatalog
from k1link.web.plugin_runtime import (
DevicePluginDispatcher,
DevicePluginRuntimeContribution,
PluginActionNotFoundError,
PluginNotFoundError,
)
class FakeXgridsService:
@@ -139,11 +139,11 @@ def test_composition_loads_and_dispatches_two_synthetic_plugins(
async def invoke(
self,
action_id: str,
payload: Mapping[str, Any],
invocation: RuntimeActionInvocation,
) -> dict[str, Any]:
assert action_id == "state.read"
assert payload == {}
assert invocation.plugin_id == self.plugin_id
assert invocation.action_id == "state.read"
assert invocation.parameters == {}
return {"plugin_id": self.plugin_id}
closed: list[str] = []
@@ -346,6 +346,8 @@ def test_plugin_sdk_v0alpha2_exports_closed_json_schemas_and_vocabulary() -> Non
"DeviceInstanceRef",
"DeviceSessionSnapshot",
"OperationEvent",
"RuntimeActionInvocation",
"RuntimeActionResult",
"CanonicalStreamEnvelope",
"EvidenceRecord",
"CompatibilityAssessment",
+4 -4
View File
@@ -10,16 +10,16 @@ import numpy as np
import pytest
from k1link.data_plane import DecodedDataPlaneView, NormalizationError
from k1link.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.protocol.normalizer import normalize_k1_message
from k1link.viewer.messages import StreamMessage
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.device_plugins.xgrids_k1.protocol.normalizer import normalize_k1_message
from k1link.device_plugins.xgrids_k1.viewer.messages import StreamMessage
from k1link.device_plugins.xgrids_k1.viewer.runtime import VisualizationRuntime
from k1link.viewer.rerun_bridge import (
RerunBridge,
RerunSceneSettings,
_live_time_panel,
_point_colors,
)
from k1link.viewer.runtime import VisualizationRuntime
class FakeRecording:
+4 -4
View File
@@ -10,10 +10,9 @@ from pathlib import Path
import pytest
import k1link.viewer.rrd_export as export_module
from k1link.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.viewer.rerun_bridge import RerunSceneSettings
from k1link.viewer.rrd_export import (
import k1link.device_plugins.xgrids_k1.rrd_export as export_module
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.device_plugins.xgrids_k1.rrd_export import (
RECORDED_POINTS_VISUALIZER_ID,
RECORDED_ROOT_CONTAINER_ID,
RECORDED_SPATIAL_VIEW_ID,
@@ -24,6 +23,7 @@ from k1link.viewer.rrd_export import (
export_k1mqtt_to_rrd,
recorded_blueprint_rrd,
)
from k1link.viewer.rerun_bridge import RerunSceneSettings
def _point_payload(x: float) -> bytes:
+21 -20
View File
@@ -16,7 +16,8 @@ from fastapi import APIRouter, HTTPException, Response
from fastapi.routing import APIRoute
import k1link.sessions.media as recorded_media_module
from k1link.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.sessions import (
MaterializedRecording,
RecordedMediaInspector,
@@ -27,8 +28,8 @@ from k1link.sessions import (
SessionStore,
)
from k1link.sessions.media import _manifest_generation_sha256
from k1link.viewer.recorded import recorded_blueprint_rrd
from k1link.viewer.rerun_bridge import RerunSceneSettings
from k1link.viewer.rrd_export import recorded_blueprint_rrd
from k1link.web.camera_archive import CameraArchiveWriter
from k1link.web.session_api import (
LayoutPutRequest,
@@ -195,7 +196,7 @@ def test_session_router_lists_details_and_dispatches_opaque_replay(tmp_path: Pat
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))
observed: list[ReplayCommand] = []
async def launch(command: ReplayCommand) -> dict[str, Any]:
@@ -238,7 +239,7 @@ def test_session_router_lists_details_and_dispatches_opaque_replay(tmp_path: Pat
"dispatched": True,
}
assert replay["runtime_state"]["source_mode"] == "replay"
assert observed[0].source_path.name == "mqtt.raw.k1mqtt"
assert observed[0].primary_artifact.path.name == "mqtt.raw.k1mqtt"
for value in (listing, detail, replay):
assert_no_local_paths(value, repository)
@@ -250,7 +251,7 @@ def test_session_router_returns_seekable_recording_and_serves_byte_ranges(
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))
payload = b"RRD-seekable-recording"
export_calls = 0
@@ -354,7 +355,7 @@ def test_cold_replay_returns_quick_202_then_status_returns_ready_launch(
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))
started = threading.Event()
release = threading.Event()
calls = 0
@@ -579,7 +580,7 @@ def test_recording_response_releases_pin_once_when_asgi_send_fails(
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))
recording_path = tmp_path / "pinned.rrd"
payload = b"RRD-pinned-response"
recording_path.write_bytes(payload)
@@ -663,7 +664,7 @@ def test_preparation_status_and_cancel_are_bound_to_exact_job_etag(tmp_path: Pat
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))
started = threading.Event()
release = threading.Event()
@@ -740,7 +741,7 @@ def test_stale_status_poll_does_not_create_a_replacement_job(tmp_path: Path) ->
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))
calls = 0
def exporter(_source: Path, _destination: Path) -> dict[str, object]:
@@ -782,7 +783,7 @@ def test_ready_post_uses_each_callers_playback_without_mutating_shared_job(
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))
calls = 0
def exporter(source: Path, destination: Path) -> dict[str, object]:
@@ -849,7 +850,7 @@ def test_production_router_never_materializes_recording_inline(tmp_path: Path) -
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))
calls = 0
def exporter(_source: Path, _destination: Path) -> dict[str, object]:
@@ -887,7 +888,7 @@ def test_recorded_blueprint_endpoint_is_small_strict_and_session_scoped(
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))
observed_settings: list[RerunSceneSettings] = []
def capture_settings(settings: RerunSceneSettings, **kwargs: Any) -> bytes:
@@ -977,7 +978,7 @@ def test_session_router_exposes_opaque_recorded_media_manifest_and_ranges(
writer.close()
store = SessionStore(repository, data_dir=tmp_path / "data")
store.import_legacy_viewer_live(sessions)
store.reconcile_archive(xgrids_k1_archive_source(sessions))
payload = b"recording"
def export_recording(source: Path, destination: Path) -> dict[str, object]:
@@ -1189,7 +1190,7 @@ def test_ready_launch_uses_background_prepared_camera_manifest_without_rescan(
)
writer.close()
store = SessionStore(repository, data_dir=tmp_path / "data")
store.import_legacy_viewer_live(sessions)
store.reconcile_archive(xgrids_k1_archive_source(sessions))
def exporter(source: Path, destination: Path) -> dict[str, object]:
payload = b"prepared-recording"
@@ -1300,7 +1301,7 @@ def test_recorded_media_epoch_ends_are_monotonic_generation_bound_and_not_rrd_pa
)
second.close()
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)
artifact = store.list_recorded_media(session.name)[0]
manifest = RecordedMediaInspector().inspect(artifact, command)
@@ -1353,7 +1354,7 @@ def test_recorded_media_rejects_non_monotonic_segments_and_overlapping_epochs(
)
writer.close()
store = SessionStore(non_monotonic_root, data_dir=tmp_path / "non-monotonic-data")
store.import_legacy_viewer_live(non_monotonic_root / "sessions")
store.reconcile_archive(xgrids_k1_archive_source(non_monotonic_root / "sessions"))
with pytest.raises(SessionIntegrityError, match="strictly monotonic"):
RecordedMediaInspector().inspect(
store.list_recorded_media(non_monotonic_session.name)[0],
@@ -1384,7 +1385,7 @@ def test_recorded_media_rejects_non_monotonic_segments_and_overlapping_epochs(
)
second.close()
store = SessionStore(overlap_root, data_dir=tmp_path / "overlap-data")
store.import_legacy_viewer_live(overlap_root / "sessions")
store.reconcile_archive(xgrids_k1_archive_source(overlap_root / "sessions"))
with pytest.raises(SessionIntegrityError, match="overlap"):
RecordedMediaInspector().inspect(
store.list_recorded_media(overlap_session.name)[0],
@@ -1403,7 +1404,7 @@ def test_background_preparation_fails_closed_on_unparseable_recorded_media(
writer.append("media", b"not-an-iso-bmff-fragment")
writer.close()
store = SessionStore(repository, data_dir=tmp_path / "data")
store.import_legacy_viewer_live(sessions)
store.reconcile_archive(xgrids_k1_archive_source(sessions))
def exporter(source: Path, destination: Path) -> dict[str, object]:
return {
@@ -1463,7 +1464,7 @@ def test_background_preparation_rejects_camera_outside_rrd_timeline(
)
writer.close()
store = SessionStore(repository, data_dir=tmp_path / "data")
store.import_legacy_viewer_live(sessions)
store.reconcile_archive(xgrids_k1_archive_source(sessions))
def exporter(source: Path, destination: Path) -> dict[str, object]:
payload = b"short-spatial-recording"
@@ -1526,7 +1527,7 @@ def test_recorded_media_preparation_sidecar_reuses_and_rebuilds_generation(
)
writer.close()
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)
artifact = store.list_recorded_media(session.name)[0]
cache_root = tmp_path / "prepared-media"
+8 -7
View File
@@ -13,7 +13,8 @@ import pytest
from fastapi import APIRouter, HTTPException
from fastapi.routing import APIRoute
from k1link.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.device_plugins.xgrids_k1 import xgrids_k1_archive_source
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.sessions import (
SessionRecordingMaterializer,
SessionRecordingPreparationManager,
@@ -99,7 +100,7 @@ def test_list_refresh_discovers_new_completed_session_without_server_restart(
def refresh() -> tuple[str, ...]:
nonlocal refresh_calls
refresh_calls += 1
return store.import_legacy_viewer_live(sessions)
return store.reconcile_archive(xgrids_k1_archive_source(sessions))
router = build_session_router(store, catalog_refresher=refresh)
list_route = _endpoint(router, "/api/v1/observation-sessions", "GET")
@@ -122,7 +123,7 @@ def test_detail_and_replay_refresh_catalog_before_lookup(tmp_path: Path) -> None
def refresh() -> tuple[str, ...]:
nonlocal refresh_calls
refresh_calls += 1
return store.import_legacy_viewer_live(sessions)
return store.reconcile_archive(xgrids_k1_archive_source(sessions))
router = build_session_router(store, catalog_refresher=refresh)
detail_route = _endpoint(
@@ -167,7 +168,7 @@ def test_startup_warmup_enqueues_every_finalized_replayable_session(
sessions = repository / "sessions"
session = _make_completed_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))
def exporter(source: Path, destination: Path) -> dict[str, object]:
payload = b"startup-prepared-recording"
@@ -211,7 +212,7 @@ def test_reconciliation_skips_one_stale_session_and_prepares_later_valid_session
good = _make_completed_session(sessions, "20260716T205632Z_viewer_live")
stale = _make_completed_session(sessions, "20260716T205633Z_viewer_live")
store = SessionStore(repository, data_dir=tmp_path / "data")
store.import_legacy_viewer_live(sessions)
store.reconcile_archive(xgrids_k1_archive_source(sessions))
(stale / "captures" / "mqtt_live" / "mqtt.raw.k1mqtt").unlink()
def exporter(source: Path, destination: Path) -> dict[str, object]:
@@ -254,7 +255,7 @@ def test_reconciler_requeues_only_a_restart_interrupted_job(
sessions = repository / "sessions"
session = _make_completed_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))
started = threading.Event()
release = threading.Event()
calls = 0
@@ -321,7 +322,7 @@ def test_reconciler_does_not_loop_retry_a_genuine_failed_job(
sessions = repository / "sessions"
session = _make_completed_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))
calls = 0
def exporter(_source: Path, _destination: Path) -> dict[str, object]:
+23 -5
View File
@@ -9,7 +9,7 @@ from pathlib import Path
import pytest
from k1link.sessions.models import ReplayCommand
from k1link.sessions.models import ReplayArtifact, ReplayCommand
from k1link.sessions.preparation import (
RecordingPreparationSnapshot,
SessionRecordingPreparationManager,
@@ -25,12 +25,30 @@ def _command(root: Path, session_id: str = "20260717T100000Z_viewer_live") -> Re
metadata.write_text('{"record_type":"message","sequence":1}\n', encoding="utf-8")
return ReplayCommand(
session_id=session_id,
source_path=source,
plugin_id="test.recording-plugin",
allowed_root=root,
session_root=root,
replay_byte_length=source.stat().st_size,
metadata_byte_length=metadata.stat().st_size,
expected_source_sha256=None,
primary_artifact_id="primary",
artifacts=(
ReplayArtifact(
artifact_id="primary",
path=source,
media_type="application/x-test-recording",
file_byte_length=source.stat().st_size,
replay_byte_length=source.stat().st_size,
expected_sha256=None,
),
ReplayArtifact(
artifact_id="index",
path=metadata,
media_type="application/x-ndjson",
file_byte_length=metadata.stat().st_size,
replay_byte_length=metadata.stat().st_size,
expected_sha256=None,
),
),
timeline_origin_epoch_ns=0,
timeline_origin_monotonic_ns=0,
speed=1.0,
loop=False,
)
+60 -23
View File
@@ -13,7 +13,7 @@ from types import SimpleNamespace
import pytest
import k1link.sessions.recording as recording_module
from k1link.sessions.models import ReplayCommand
from k1link.sessions.models import ReplayArtifact, ReplayCommand
from k1link.sessions.recording import (
CACHE_SCHEMA,
RERUN_RECORDING_MEDIA_TYPE,
@@ -57,17 +57,46 @@ def _command(tmp_path: Path) -> ReplayCommand:
metadata.write_text('{"record_type":"message","sequence":1}\n', encoding="utf-8")
return ReplayCommand(
session_id="20260716T205632Z_viewer_live",
source_path=source,
plugin_id="test.recording-plugin",
allowed_root=tmp_path,
session_root=tmp_path,
replay_byte_length=source.stat().st_size,
metadata_byte_length=metadata.stat().st_size,
expected_source_sha256=None,
primary_artifact_id="primary",
artifacts=(
ReplayArtifact(
artifact_id="primary",
path=source,
media_type="application/x-test-recording",
file_byte_length=source.stat().st_size,
replay_byte_length=source.stat().st_size,
expected_sha256=None,
),
ReplayArtifact(
artifact_id="index",
path=metadata,
media_type="application/x-ndjson",
file_byte_length=metadata.stat().st_size,
replay_byte_length=metadata.stat().st_size,
expected_sha256=None,
),
),
timeline_origin_epoch_ns=0,
timeline_origin_monotonic_ns=0,
speed=1.0,
loop=False,
)
def _replace_primary(command: ReplayCommand, **changes: object) -> ReplayCommand:
replacement = replace(command.primary_artifact, **changes)
return replace(
command,
artifacts=tuple(
replacement if artifact.artifact_id == command.primary_artifact_id else artifact
for artifact in command.artifacts
),
)
def test_materializer_reuses_only_a_digest_validated_private_cache(tmp_path: Path) -> None:
command = _command(tmp_path)
exporter = FakeExporter()
@@ -137,8 +166,12 @@ def test_failed_rebuild_preserves_previously_published_cache(tmp_path: Path) ->
published_bytes = first.path.read_bytes()
sidecar = first.path.with_name("scene.rrd.cache.json")
published_sidecar = sidecar.read_bytes()
command.source_path.write_bytes(b"new-source-that-requires-rebuild")
changed = replace(command, replay_byte_length=command.source_path.stat().st_size)
command.primary_artifact.path.write_bytes(b"new-source-that-requires-rebuild")
changed = _replace_primary(
command,
file_byte_length=command.primary_artifact.path.stat().st_size,
replay_byte_length=command.primary_artifact.path.stat().st_size,
)
def fail_export(_source: Path, destination: Path) -> dict[str, object]:
destination.write_bytes(b"incomplete-candidate")
@@ -173,11 +206,15 @@ def test_materializer_rebuilds_when_source_or_recording_changes(tmp_path: Path)
assert repaired.sha256 == _sha256(repaired.path)
assert repaired.sha256 != hashlib.sha256(corrupted).hexdigest()
command.source_path.write_bytes(b"new-native-source")
command = replace(command, replay_byte_length=command.source_path.stat().st_size)
command.primary_artifact.path.write_bytes(b"new-native-source")
command = _replace_primary(
command,
file_byte_length=command.primary_artifact.path.stat().st_size,
replay_byte_length=command.primary_artifact.path.stat().st_size,
)
updated = materializer.materialize(command)
assert exporter.calls == 3
assert updated.source_sha256 == _sha256(command.source_path)
assert updated.source_sha256 == _sha256(command.primary_artifact.path)
def test_concurrent_materialization_exports_one_recording(tmp_path: Path) -> None:
@@ -227,9 +264,9 @@ def test_materializer_revalidates_prepared_source_without_following_replaced_sym
exporter = FakeExporter()
materializer = SessionRecordingMaterializer(tmp_path / "private", exporter=exporter)
outside = tmp_path / "outside.k1mqtt"
outside.write_bytes(command.source_path.read_bytes())
command.source_path.unlink()
command.source_path.symlink_to(outside)
outside.write_bytes(command.primary_artifact.path.read_bytes())
command.primary_artifact.path.unlink()
command.primary_artifact.path.symlink_to(outside)
with pytest.raises(RecordingMaterializationError, match="missing or unsafe"):
materializer.materialize(command)
@@ -242,11 +279,11 @@ def test_materializer_exports_only_validated_prefix_before_crash_tail(
tmp_path: Path,
) -> None:
command = _command(tmp_path / "session")
committed_raw_bytes = command.replay_byte_length
committed_metadata_bytes = command.metadata_byte_length
with command.source_path.open("ab") as stream:
committed_raw_bytes = command.primary_artifact.replay_byte_length
committed_metadata_bytes = command.artifacts[1].replay_byte_length
with command.primary_artifact.path.open("ab") as stream:
stream.write(b"uncommitted-raw-tail")
metadata = command.source_path.with_name("mqtt.metadata.jsonl")
metadata = command.primary_artifact.path.with_name("mqtt.metadata.jsonl")
with metadata.open("ab") as stream:
stream.write(b'{"record_type":"message"')
observed: list[tuple[int, int]] = []
@@ -265,7 +302,7 @@ def test_materializer_exports_only_validated_prefix_before_crash_tail(
assert observed == [(committed_raw_bytes, committed_metadata_bytes)]
assert recording.source_sha256 == hashlib.sha256(b"native-source-recording").hexdigest()
assert command.source_path.read_bytes().endswith(b"uncommitted-raw-tail")
assert command.primary_artifact.path.read_bytes().endswith(b"uncommitted-raw-tail")
def test_global_singleflight_bounds_exports_across_different_sessions(tmp_path: Path) -> None:
@@ -447,14 +484,14 @@ def test_cache_quota_evicts_lru_derived_recording_without_deleting_raw(
free_space_reserve_bytes=0,
)
first_recording = materializer.materialize(first)
first_source_bytes = first.source_path.read_bytes()
first_source_bytes = first.primary_artifact.path.read_bytes()
second_recording = materializer.materialize(second)
assert not first_recording.path.exists()
assert second_recording.path.exists()
assert first.source_path.read_bytes() == first_source_bytes
assert second.source_path.exists()
assert first.primary_artifact.path.read_bytes() == first_source_bytes
assert second.primary_artifact.path.exists()
def test_pinned_cache_entry_survives_eviction_until_release(tmp_path: Path) -> None:
@@ -503,7 +540,7 @@ def test_cache_free_space_reserve_refuses_export_without_touching_raw(
monkeypatch: pytest.MonkeyPatch,
) -> None:
command = _command(tmp_path / "session")
source_bytes = command.source_path.read_bytes()
source_bytes = command.primary_artifact.path.read_bytes()
materializer = SessionRecordingMaterializer(
tmp_path / "private",
exporter=FakeExporter(),
@@ -519,4 +556,4 @@ def test_cache_free_space_reserve_refuses_export_without_touching_raw(
with pytest.raises(RecordingMaterializationError, match="free-space reserve"):
materializer.materialize(command)
assert command.source_path.read_bytes() == source_bytes
assert command.primary_artifact.path.read_bytes() == source_bytes
+63 -26
View File
@@ -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"
+6 -2
View File
@@ -6,8 +6,12 @@ import struct
import lz4.block
import pytest
from k1link.protocol.protobuf_wire import ProtobufWireError, decode_zigzag64, iter_fields
from k1link.protocol.streams import (
from k1link.device_plugins.xgrids_k1.protocol.protobuf_wire import (
ProtobufWireError,
decode_zigzag64,
iter_fields,
)
from k1link.device_plugins.xgrids_k1.protocol.streams import (
DecodeLimits,
StreamDecodeError,
UnsupportedCompressionError,
+3 -3
View File
@@ -10,9 +10,9 @@ import lz4.block
import pytest
from typer.testing import CliRunner
from k1link.analyze.stream_summary import summarize_mqtt_streams
from k1link.cli import app
from k1link.mqtt.capture import FRAME_HEADER, RAW_MAGIC, CaptureFormatError
from k1link.device_plugins.xgrids_k1.analyze.stream_summary import summarize_mqtt_streams
from k1link.device_plugins.xgrids_k1.cli import app
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC, CaptureFormatError
runner = CliRunner()
+3 -3
View File
@@ -7,8 +7,8 @@ from pathlib import Path
import pytest
from typer.testing import CliRunner
from k1link.cli import app
from k1link.usb.snapshot import (
from k1link.device_plugins.xgrids_k1.cli import app
from k1link.device_plugins.xgrids_k1.usb.snapshot import (
DISKUTIL_COMMAND,
SERIAL_IOREG_COMMAND,
USB_IOREG_COMMAND,
@@ -242,7 +242,7 @@ def test_usb_snapshot_cli_writes_json(
"device_writes_performed": False,
},
}
monkeypatch.setattr("k1link.cli.usb_snapshot", lambda: payload)
monkeypatch.setattr("k1link.device_plugins.xgrids_k1.cli.usb_snapshot", lambda: payload)
result = runner.invoke(app, ["usb", "snapshot", "--out", str(output)])
+6 -2
View File
@@ -5,8 +5,12 @@ from pathlib import Path
import pytest
from k1link.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.viewer.replay import ReplayFormatError, detect_replay_format, iter_replay_messages
from k1link.device_plugins.xgrids_k1.mqtt.capture import FRAME_HEADER, RAW_MAGIC
from k1link.device_plugins.xgrids_k1.viewer.replay import (
ReplayFormatError,
detect_replay_format,
iter_replay_messages,
)
def _write_native(path: Path, topic: str, payload: bytes) -> None:
+1 -1
View File
@@ -6,7 +6,7 @@ from typing import Any
from pytest import MonkeyPatch
import k1link.web.xgrids_k1_facade as xgrids_backend
import k1link.device_plugins.xgrids_k1.facade as xgrids_backend
def test_ble_scan_exposes_every_device_and_only_labels_likely_k1(
+1 -1
View File
@@ -6,8 +6,8 @@ from typing import Any
import pytest
from k1link.device_plugins.xgrids_k1.facade import XGRIDS_K1_PLUGIN_ID
from k1link.web.app import INVALID_REQUEST_DETAIL, app
from k1link.web.xgrids_k1_facade import XGRIDS_K1_PLUGIN_ID
async def _post_json(path: str, payload: dict[str, Any]) -> tuple[int, str]:
+1 -1
View File
@@ -1,6 +1,6 @@
import pytest
from k1link.ble.wifi_provisioning import (
from k1link.device_plugins.xgrids_k1.ble.wifi_provisioning import (
FRAME_LENGTH,
build_wifi_provisioning_frame,
parse_wifi_status,
+2 -2
View File
@@ -7,8 +7,8 @@ from typing import Any
import pytest
from pydantic import SecretStr
import k1link.web.xgrids_k1_facade as facade_module
from k1link.web.xgrids_k1_facade import (
import k1link.device_plugins.xgrids_k1.facade as facade_module
from k1link.device_plugins.xgrids_k1.facade import (
DEFAULT_LIVE_STREAMS,
XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
AbortAcquisitionRequest,
+3 -3
View File
@@ -9,14 +9,14 @@ from pathlib import Path
import pytest
import k1link.web.xgrids_k1_camera as camera_module
from k1link.web.xgrids_k1_camera import (
import k1link.device_plugins.xgrids_k1.camera as camera_module
from k1link.device_plugins.xgrids_k1.camera import (
CAMERA_MEDIA_TYPE,
XgridsK1CameraGateway,
_build_ffmpeg_argv,
_read_mp4_box,
)
from k1link.web.xgrids_k1_facade import (
from k1link.device_plugins.xgrids_k1.facade import (
XGRIDS_K1_COMPATIBILITY_PROFILE_ID,
XGRIDS_K1_PLUGIN_ID,
CameraPreviewSelectRequest,