feat(k1): complete primary acquisition lifecycle
This commit is contained in:
+189
-9
@@ -100,14 +100,65 @@ def make_legacy_session(
|
||||
return session
|
||||
|
||||
|
||||
def add_capture_clock(session: Path, *, scope: str = "session") -> Path:
|
||||
capture = session / "captures" / "mqtt_live"
|
||||
origin_document = {
|
||||
"schema_version": 1,
|
||||
"started_at_epoch_ns": 1_784_235_391_699_000_000,
|
||||
"started_monotonic_ns": 9_000_000_000,
|
||||
}
|
||||
origin_path = capture / "mqtt.timeline.origin.json"
|
||||
origin_path.write_text(
|
||||
json.dumps(origin_document, separators=(",", ":")) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
clock_document = {
|
||||
**origin_document,
|
||||
"completed_at_epoch_ns": 1_784_235_394_699_000_000,
|
||||
"completed_monotonic_ns": 12_000_000_000,
|
||||
}
|
||||
clock_payload = (json.dumps(clock_document, separators=(",", ":")) + "\n").encode()
|
||||
clock_digest = hashlib.sha256(clock_payload).hexdigest()
|
||||
provisional_path = capture / "mqtt.timeline.json"
|
||||
provisional_path.write_bytes(clock_payload)
|
||||
clock_path = (
|
||||
capture / f"mqtt.timeline.session-{clock_digest}.json"
|
||||
if scope == "session"
|
||||
else provisional_path
|
||||
)
|
||||
if clock_path != provisional_path:
|
||||
clock_path.write_bytes(clock_payload)
|
||||
summary_path = capture / "mqtt.summary.json"
|
||||
summary = json.loads(summary_path.read_text(encoding="utf-8"))
|
||||
summary["schema_version"] = 2
|
||||
summary["capture_clock_scope"] = scope
|
||||
summary["session_elapsed_seconds"] = 3.0
|
||||
summary["artifacts"] = {
|
||||
"raw": "mqtt.raw.k1mqtt",
|
||||
"metadata_jsonl": "mqtt.metadata.jsonl",
|
||||
"capture_clock_origin": origin_path.name,
|
||||
"capture_clock": clock_path.name,
|
||||
"summary": "mqtt.summary.json",
|
||||
}
|
||||
summary["artifact_hashes"]["capture_clock_origin_sha256"] = hashlib.sha256(
|
||||
origin_path.read_bytes()
|
||||
).hexdigest()
|
||||
summary["artifact_hashes"]["capture_clock_sha256"] = hashlib.sha256(
|
||||
clock_path.read_bytes()
|
||||
).hexdigest()
|
||||
summary_path.write_text(json.dumps(summary), encoding="utf-8")
|
||||
return clock_path
|
||||
|
||||
|
||||
def test_evidence_root_is_private_and_configurable(
|
||||
tmp_path: Path,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
repository = tmp_path / "repo"
|
||||
assert resolve_missioncore_evidence_dir(repository) == (
|
||||
repository / ".runtime" / "mission-core" / "evidence" / "sessions"
|
||||
).resolve()
|
||||
assert (
|
||||
resolve_missioncore_evidence_dir(repository)
|
||||
== (repository / ".runtime" / "mission-core" / "evidence" / "sessions").resolve()
|
||||
)
|
||||
|
||||
configured = tmp_path / "external-evidence"
|
||||
monkeypatch.setenv("MISSIONCORE_EVIDENCE_DIR", str(configured))
|
||||
@@ -127,6 +178,35 @@ def test_catalog_reconciles_sessions_removed_from_one_evidence_root(tmp_path: Pa
|
||||
assert store.list_recent().items == ()
|
||||
|
||||
|
||||
def test_catalog_uses_valid_project_name_as_session_display_name(tmp_path: Path) -> None:
|
||||
sessions = tmp_path / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
manifest_path = session / "manifest.redacted.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
manifest["project_name"] = " K1 Route Alpha "
|
||||
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
store = SessionStore(tmp_path / "repo", data_dir=tmp_path / "data")
|
||||
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
assert store.list_recent().items[0].display_name == "K1 Route Alpha"
|
||||
|
||||
|
||||
def test_catalog_rejects_non_utf8_project_display_name(tmp_path: Path) -> None:
|
||||
sessions = tmp_path / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
manifest_path = session / "manifest.redacted.json"
|
||||
manifest_path.write_text(
|
||||
json.dumps({"project_name": "unsafe-\ud800"}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
store = SessionStore(tmp_path / "repo", data_dir=tmp_path / "data")
|
||||
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
assert store.list_recent().items[0].display_name == session.name
|
||||
|
||||
|
||||
def make_recorded_camera_source(
|
||||
session: Path,
|
||||
source_id: str = "sensor.camera.left",
|
||||
@@ -191,8 +271,7 @@ def replace_summary_with_recovery_metadata(
|
||||
}
|
||||
)
|
||||
metadata = b"".join(
|
||||
(json.dumps(record, separators=(",", ":")) + "\n").encode("utf-8")
|
||||
for record in records
|
||||
(json.dumps(record, separators=(",", ":")) + "\n").encode("utf-8") for record in records
|
||||
)
|
||||
if corrupt_trailing_line:
|
||||
metadata += b'{"record_type":"message"'
|
||||
@@ -243,6 +322,109 @@ def test_store_uses_private_wal_database_and_idempotently_imports_legacy_session
|
||||
assert store.database_path.stat().st_mode & 0o777 == 0o600
|
||||
|
||||
|
||||
def test_completed_capture_clock_is_a_digest_bound_replay_artifact(tmp_path: Path) -> None:
|
||||
sessions = tmp_path / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
clock_path = add_capture_clock(session)
|
||||
store = SessionStore(tmp_path / "repo", data_dir=tmp_path / "data")
|
||||
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
assert store.get_session(session.name).summary.duration_seconds == 3.0
|
||||
command = store.prepare_replay(session.name)
|
||||
clock_artifact = next(
|
||||
artifact for artifact in command.artifacts if artifact.artifact_id == "raw-transport-clock"
|
||||
)
|
||||
assert command.timeline_origin_monotonic_ns == 9_000_000_000
|
||||
assert command.timeline_origin_epoch_ns == 1_784_235_391_699_000_000
|
||||
assert clock_artifact.path == clock_path
|
||||
assert clock_artifact.replay_byte_length == clock_path.stat().st_size
|
||||
assert clock_artifact.expected_sha256 == hashlib.sha256(clock_path.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def test_completed_capture_rejects_corrupt_declared_clock(tmp_path: Path) -> None:
|
||||
sessions = tmp_path / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
clock_path = add_capture_clock(session)
|
||||
clock_path.write_text('{"schema_version":1,"tampered":true}\n', encoding="utf-8")
|
||||
store = SessionStore(tmp_path / "repo", data_dir=tmp_path / "data")
|
||||
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
assert detail.summary.status == "failed"
|
||||
assert detail.summary.replayable is False
|
||||
|
||||
|
||||
def test_completed_capture_rejects_clock_filename_with_wrong_digest_suffix(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
sessions = tmp_path / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
clock_path = add_capture_clock(session)
|
||||
wrong_path = clock_path.with_name(f"mqtt.timeline.session-{'0' * 64}.json")
|
||||
wrong_path.write_bytes(clock_path.read_bytes())
|
||||
summary_path = session / "captures" / "mqtt_live" / "mqtt.summary.json"
|
||||
summary = json.loads(summary_path.read_text(encoding="utf-8"))
|
||||
summary["artifacts"]["capture_clock"] = wrong_path.name
|
||||
summary_path.write_text(json.dumps(summary), encoding="utf-8")
|
||||
store = SessionStore(tmp_path / "repo", data_dir=tmp_path / "data")
|
||||
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
assert detail.summary.status == "failed"
|
||||
assert detail.summary.replayable is False
|
||||
|
||||
|
||||
def test_camera_session_does_not_advertise_provisional_transport_clock(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
sessions = tmp_path / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
add_capture_clock(session, scope="transport")
|
||||
make_recorded_camera_source(session)
|
||||
store = SessionStore(tmp_path / "repo", data_dir=tmp_path / "data")
|
||||
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
assert detail.summary.replayable is False
|
||||
|
||||
|
||||
def test_interrupted_camera_session_uses_durable_origin_and_fails_closed(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
sessions = tmp_path / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
add_capture_clock(session, scope="transport")
|
||||
replace_summary_with_recovery_metadata(session)
|
||||
make_recorded_camera_source(session)
|
||||
store = SessionStore(tmp_path / "repo", data_dir=tmp_path / "data")
|
||||
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
detail = store.get_session(session.name)
|
||||
assert detail.summary.status == "interrupted"
|
||||
assert detail.summary.replayable is False
|
||||
|
||||
|
||||
def test_interrupted_raw_replay_preserves_durable_pre_message_origin(tmp_path: Path) -> None:
|
||||
sessions = tmp_path / "sessions"
|
||||
session = make_legacy_session(sessions, "20260716T205632Z_viewer_live")
|
||||
add_capture_clock(session, scope="transport")
|
||||
replace_summary_with_recovery_metadata(session)
|
||||
store = SessionStore(tmp_path / "repo", data_dir=tmp_path / "data")
|
||||
|
||||
store.reconcile_archive(xgrids_k1_archive_source(sessions))
|
||||
|
||||
command = store.prepare_replay(session.name)
|
||||
assert command.timeline_origin_monotonic_ns == 9_000_000_000
|
||||
assert any(
|
||||
artifact.artifact_id == "raw-transport-clock-origin" for artifact in command.artifacts
|
||||
)
|
||||
|
||||
|
||||
def test_reconcile_claims_pre_plugin_catalog_row_without_changing_session_identity(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
@@ -260,8 +442,7 @@ def test_reconcile_claims_pre_plugin_catalog_row_without_changing_session_identi
|
||||
(session.name,),
|
||||
)
|
||||
connection.execute(
|
||||
"UPDATE observation_session_artifacts SET replay_byte_length = 0 "
|
||||
"WHERE session_id = ?",
|
||||
"UPDATE observation_session_artifacts SET replay_byte_length = 0 WHERE session_id = ?",
|
||||
(session.name,),
|
||||
)
|
||||
connection.commit()
|
||||
@@ -511,8 +692,7 @@ def test_interrupted_capture_replays_only_committed_prefix_before_partial_raw_ta
|
||||
assert detail.summary.replayable is True
|
||||
assert command.primary_artifact.replay_byte_length == committed_bytes
|
||||
assert (
|
||||
command.primary_artifact.replay_byte_length
|
||||
< command.primary_artifact.path.stat().st_size
|
||||
command.primary_artifact.replay_byte_length < command.primary_artifact.path.stat().st_size
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user