feat(plugins): isolate device integrations
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user