Добавление канонического графа M4.7
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILDER_PATH = REPOSITORY_ROOT / "scripts/build_m47_worker_graph_shadow_artifact.py"
|
||||
SPEC = importlib.util.spec_from_file_location("m47_worker_graph_shadow_builder", BUILDER_PATH)
|
||||
assert SPEC is not None and SPEC.loader is not None
|
||||
BUILDER = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(BUILDER)
|
||||
|
||||
|
||||
def _sha256(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def _regular_files(archive: tarfile.TarFile) -> dict[str, bytes]:
|
||||
result: dict[str, bytes] = {}
|
||||
for member in archive.getmembers():
|
||||
if not member.isfile():
|
||||
continue
|
||||
extracted = archive.extractfile(member)
|
||||
assert extracted is not None
|
||||
result[member.name] = extracted.read()
|
||||
return result
|
||||
|
||||
|
||||
def test_m47_worker_artifact_is_deterministic_and_self_contained(tmp_path: Path) -> None:
|
||||
patch_id = "mission-core-m47-graph-shadow-unit-001"
|
||||
revision = "d" * 40
|
||||
first = BUILDER.build_artifact(patch_id, tmp_path / "first", revision=revision)
|
||||
second = BUILDER.build_artifact(patch_id, tmp_path / "second", revision=revision)
|
||||
first_bytes = Path(first["artifact"]).read_bytes()
|
||||
second_bytes = Path(second["artifact"]).read_bytes()
|
||||
|
||||
assert first_bytes == second_bytes
|
||||
assert first["sha256"] == _sha256(first_bytes)
|
||||
assert first["transition"] == "m47-canonical-graph-shadow-v1"
|
||||
with tarfile.open(first["artifact"], "r:gz") as archive:
|
||||
regular = _regular_files(archive)
|
||||
payload_names = sorted(
|
||||
name.removeprefix("payload/")
|
||||
for name in regular
|
||||
if name.startswith("payload/")
|
||||
)
|
||||
assert payload_names == first["payload_files"]
|
||||
assert regular["files.txt"].decode().splitlines() == first["payload_files"]
|
||||
assert _sha256(regular[f"payload/{BUILDER.WHEEL_NAME}"]) == first["wheel_sha256"]
|
||||
assert regular[f"payload/{BUILDER.RUNNER.name}"] == BUILDER.RUNNER.read_bytes()
|
||||
for relative in BUILDER.CONFIG_PATHS:
|
||||
assert regular[f"payload/{relative.name}"] == (
|
||||
BUILDER.REPOSITORY_ROOT / relative
|
||||
).read_bytes()
|
||||
|
||||
|
||||
def test_m47_descriptor_preserves_predecessor_and_separates_readiness() -> None:
|
||||
descriptor = json.loads(
|
||||
BUILDER.render_descriptor(
|
||||
"mission-core-m47-graph-shadow-unit-002",
|
||||
"e" * 40,
|
||||
wheel_sha256="f" * 64,
|
||||
)
|
||||
)
|
||||
|
||||
assert descriptor["schema_version"] == "nodedc.mission-core-worker.shadow-release/v2"
|
||||
assert descriptor["transition"] == "m47-canonical-graph-shadow-v1"
|
||||
assert descriptor["boundary"]["external_deploy_registry"] is False
|
||||
assert descriptor["container"]["public_ports"] is False
|
||||
assert descriptor["inputs"]["local_surface"]["sha256"] == (
|
||||
"f57eb2485b6cef47f2a97a2d9ff1aa9fd9265fe1eb69cd5852d12f39e13b8bc6"
|
||||
)
|
||||
assert descriptor["inputs"]["accepted_temporal_frames"]["sha256"] == (
|
||||
"e83b80ea06b3462c2d04c5a1b74289a0ec401596c7150ae90f5a1b748b639c3a"
|
||||
)
|
||||
assert descriptor["inputs"]["accepted_threat_frames"]["sha256"] == (
|
||||
"b57be1839f5915e3b80b54355b694e0bd8c9ac318d0cbe6de2bff713082cfa4e"
|
||||
)
|
||||
assert descriptor["predecessor"]["durable_worker"]["name"] == (
|
||||
"ndc-mission-core-perception-worker"
|
||||
)
|
||||
assert descriptor["rollback"] == {
|
||||
"durable_worker_action": "none",
|
||||
"preserve_failed_evidence": True,
|
||||
"remove_candidate_container": True,
|
||||
"remove_unaccepted_release": True,
|
||||
"triton_action": "none",
|
||||
}
|
||||
assert descriptor["acceptance"] == {
|
||||
"run_mode": "lossless-replay",
|
||||
"expected_frames": 4489,
|
||||
"delivered_frames": 4489,
|
||||
"failed_frames": 0,
|
||||
"stale_frames": 0,
|
||||
"superseded_frames": 0,
|
||||
"accepted_parity": True,
|
||||
"class_routing_used": False,
|
||||
}
|
||||
assert descriptor["readiness"]["graph"] == {
|
||||
"graph_id": "reference-perception-graph/v2",
|
||||
"terminal_accounting_required": True,
|
||||
"actuation_allowed": False,
|
||||
}
|
||||
assert set(descriptor["release"]["configs"]) == {
|
||||
path.name for path in BUILDER.CONFIG_PATHS
|
||||
}
|
||||
serialized = json.dumps(descriptor).encode()
|
||||
assert b"PRIVATE KEY" not in serialized
|
||||
assert b"password=" not in serialized.lower()
|
||||
|
||||
|
||||
def test_m47_runner_calls_only_the_canonical_graph_entrypoint() -> None:
|
||||
runner = BUILDER.RUNNER.read_text("utf-8")
|
||||
|
||||
assert "k1link.perception.reference_graph_cli" in runner
|
||||
assert "k1link.perception.detector_replay_cli" not in runner
|
||||
assert '"--mode", $descriptor.acceptance.run_mode' in runner
|
||||
assert '"--temporal-parity-frames"' in runner
|
||||
assert '"--threat-parity-frames"' in runner
|
||||
assert 'Write-Output "PROVIDER_READINESS=accepted"' in runner
|
||||
assert 'Write-Output "GRAPH_READINESS=accepted"' in runner
|
||||
assert 'Write-Output "DURABLE_WORKER_ACTION=none"' in runner
|
||||
assert 'Write-Output "TRITON_ACTION=none"' in runner
|
||||
@@ -1,11 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import tarfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
BUILDER_PATH = REPOSITORY_ROOT / "scripts/build_m4_worker_shadow_artifact.py"
|
||||
SPEC = importlib.util.spec_from_file_location("m4_worker_shadow_builder", BUILDER_PATH)
|
||||
@@ -13,98 +13,12 @@ assert SPEC is not None and SPEC.loader is not None
|
||||
BUILDER = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(BUILDER)
|
||||
|
||||
|
||||
def _sha256(value: bytes) -> str:
|
||||
return hashlib.sha256(value).hexdigest()
|
||||
|
||||
|
||||
def _regular_files(archive: tarfile.TarFile) -> dict[str, bytes]:
|
||||
result: dict[str, bytes] = {}
|
||||
for member in archive.getmembers():
|
||||
if not member.isfile():
|
||||
continue
|
||||
extracted = archive.extractfile(member)
|
||||
assert extracted is not None
|
||||
result[member.name] = extracted.read()
|
||||
return result
|
||||
|
||||
|
||||
def test_worker_shadow_artifact_is_deterministic_narrow_and_self_contained(
|
||||
def test_historical_worker_shadow_refuses_runtime_relabeling(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
patch_id = "mission-core-m4-detector-shadow-unit-001"
|
||||
first = BUILDER.build_artifact(patch_id, tmp_path / "first")
|
||||
second = BUILDER.build_artifact(patch_id, tmp_path / "second")
|
||||
first_bytes = Path(first["artifact"]).read_bytes()
|
||||
second_bytes = Path(second["artifact"]).read_bytes()
|
||||
|
||||
assert first_bytes == second_bytes
|
||||
assert first["sha256"] == _sha256(first_bytes)
|
||||
assert first["sha256"] == second["sha256"]
|
||||
assert first["payload_files"] == list(BUILDER.PAYLOAD_FILES)
|
||||
|
||||
with tarfile.open(first["artifact"], "r:gz") as archive:
|
||||
members = archive.getmembers()
|
||||
names = [member.name for member in members]
|
||||
regular = _regular_files(archive)
|
||||
|
||||
assert names == [
|
||||
"manifest.env",
|
||||
"files.txt",
|
||||
"payload",
|
||||
"payload/Invoke-M4DetectorShadow.ps1",
|
||||
"payload/m4-recorded-realtime-baseline-v1.json",
|
||||
"payload/mission-core-worker-shadow-v1.json",
|
||||
f"payload/{BUILDER.WHEEL_NAME}",
|
||||
]
|
||||
assert (
|
||||
regular["manifest.env"]
|
||||
== (f"id={patch_id}\ncomponent=mission-core-worker\ntype=shadow-release\n").encode()
|
||||
)
|
||||
assert regular["files.txt"].decode().splitlines() == list(BUILDER.PAYLOAD_FILES)
|
||||
assert regular[f"payload/{BUILDER.RUNNER_NAME}"] == BUILDER.RUNNER.read_bytes()
|
||||
assert _sha256(regular[f"payload/{BUILDER.WHEEL_NAME}"]) == (BUILDER.EXPECTED_WHEEL_SHA256)
|
||||
assert _sha256(regular["payload/m4-recorded-realtime-baseline-v1.json"]) == (
|
||||
BUILDER.EXPECTED_BASELINE_SHA256
|
||||
)
|
||||
descriptor = json.loads(regular["payload/mission-core-worker-shadow-v1.json"])
|
||||
assert descriptor["patch_id"] == patch_id
|
||||
assert descriptor["code_revision"] == first["code_revision"]
|
||||
assert descriptor["boundary"] == {
|
||||
"external_deploy_registry": False,
|
||||
"nodedc_platform_repository": False,
|
||||
"repository": "NODEDC_MISSION_CORE",
|
||||
"server_docker_runtime": False,
|
||||
}
|
||||
assert descriptor["dependency_tree_identity"] == (
|
||||
"relative-path-tab-size-tab-file-sha256-lf/v1"
|
||||
)
|
||||
assert descriptor["release"]["runner"] == {
|
||||
"name": BUILDER.RUNNER_NAME,
|
||||
"sha256": _sha256(BUILDER.RUNNER.read_bytes()),
|
||||
}
|
||||
assert descriptor["release"]["wheel"] == {
|
||||
"name": BUILDER.WHEEL_NAME,
|
||||
"sha256": BUILDER.EXPECTED_WHEEL_SHA256,
|
||||
}
|
||||
assert descriptor["container"]["public_ports"] is False
|
||||
assert descriptor["rollback"] == {
|
||||
"durable_worker_action": "none",
|
||||
"preserve_failed_evidence": True,
|
||||
"remove_candidate_container": True,
|
||||
"remove_unaccepted_release": True,
|
||||
"triton_action": "none",
|
||||
}
|
||||
all_bytes = b"\n".join(regular.values())
|
||||
assert b"PRIVATE KEY" not in all_bytes
|
||||
assert b"password=" not in all_bytes.lower()
|
||||
assert not any(
|
||||
Path(name).name.startswith(".env")
|
||||
or "/secrets/" in name
|
||||
or "/runtime/" in name
|
||||
or "/recordings/" in name
|
||||
for name in names
|
||||
)
|
||||
with pytest.raises(BUILDER.ArtifactBuildError, match="runtime wheel digest changed"):
|
||||
BUILDER.build_artifact(patch_id, tmp_path)
|
||||
|
||||
|
||||
def test_worker_shadow_descriptor_pins_external_inputs_and_preserves_e15() -> None:
|
||||
|
||||
@@ -320,6 +320,7 @@ def test_reference_graph_config_pins_all_roles_and_queue_bounds() -> None:
|
||||
providers=tuple(
|
||||
ProviderPin(role, f"{role.value}-provider", "v1", "78a3dc2", "a" * 64)
|
||||
for role in ProviderRole
|
||||
if role is not ProviderRole.ROLLING
|
||||
),
|
||||
queues=tuple(
|
||||
QueuePolicy(stage, 2, 80_000_000, 200_000_000)
|
||||
|
||||
@@ -5,6 +5,7 @@ import threading
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import replace
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from threading import Event
|
||||
|
||||
import numpy as np
|
||||
@@ -36,9 +37,12 @@ from k1link.perception.contracts import (
|
||||
TimestampBundle,
|
||||
)
|
||||
from k1link.perception.graph import (
|
||||
GraphExecutionError,
|
||||
GraphRunMode,
|
||||
GraphRunResult,
|
||||
GraphState,
|
||||
ReferencePerceptionGraphV1,
|
||||
ReferencePerceptionGraphV2,
|
||||
TerminalOutcomeType,
|
||||
)
|
||||
from k1link.perception.providers import (
|
||||
@@ -47,6 +51,7 @@ from k1link.perception.providers import (
|
||||
ProviderRole,
|
||||
QueuePolicy,
|
||||
ReferencePerceptionGraphConfig,
|
||||
ReferencePerceptionGraphConfigV2,
|
||||
SourcePacket,
|
||||
)
|
||||
from k1link.perception.recorded_source import (
|
||||
@@ -230,6 +235,44 @@ class _Motion:
|
||||
return obstacles
|
||||
|
||||
|
||||
class _Rolling:
|
||||
provider_id = "test-rolling/v1"
|
||||
|
||||
def update(
|
||||
self,
|
||||
packet: SourcePacket,
|
||||
obstacles: tuple[TemporalObstacle, ...],
|
||||
) -> tuple[TemporalObstacle, ...]:
|
||||
if packet.envelope.sequence == 0:
|
||||
return ()
|
||||
return (
|
||||
TemporalObstacle(
|
||||
component_id=f"rolling-{packet.envelope.sequence}",
|
||||
identity_scope="ephemeral",
|
||||
state=TemporalState.RETAINED,
|
||||
ttl_ns=3_000_000_000,
|
||||
last_hit_ns=packet.envelope.timestamps.source_ns - 100_000_000,
|
||||
age_ns=100_000_000,
|
||||
association_basis="registered-map-increment-retention",
|
||||
history=(
|
||||
HistorySample(
|
||||
frame_id=f"frame-{packet.envelope.sequence - 1:06d}",
|
||||
evidence_time_ns=(
|
||||
packet.envelope.timestamps.source_ns - 100_000_000
|
||||
),
|
||||
centroid_xyz_m=(3.0, 0.5, 0.5),
|
||||
),
|
||||
),
|
||||
cells=(GridCell(99, packet.envelope.sequence, 0),),
|
||||
coordinate_frame="map",
|
||||
last_centroid_xyz_m=(3.0, 0.5, 0.5),
|
||||
motion=MotionState.UNKNOWN,
|
||||
motion_confidence=0.0,
|
||||
motion_reason="retained-map-increment-no-current-motion",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class _Threat:
|
||||
provider_id = "test-threat/v1"
|
||||
|
||||
@@ -312,6 +355,39 @@ def _config(
|
||||
)
|
||||
|
||||
|
||||
def _config_v2(
|
||||
capacity: int = 8,
|
||||
terminal_timeout_ns: int = 500_000_000,
|
||||
) -> ReferencePerceptionGraphConfigV2:
|
||||
ids = {
|
||||
ProviderRole.SOURCE: _Source.provider_id,
|
||||
ProviderRole.DETECTOR: _Detector.provider_id,
|
||||
ProviderRole.GEOMETRY: _Geometry.provider_id,
|
||||
ProviderRole.TEMPORAL: _Temporal.provider_id,
|
||||
ProviderRole.MOTION: _Motion.provider_id,
|
||||
ProviderRole.ROLLING: _Rolling.provider_id,
|
||||
ProviderRole.THREAT: _Threat.provider_id,
|
||||
}
|
||||
return ReferencePerceptionGraphConfigV2(
|
||||
graph_id="reference-perception-graph/v2",
|
||||
source_profile_id=BASELINE_PROFILE_ID,
|
||||
providers=tuple(
|
||||
ProviderPin(role, provider_id, "v1", "test-revision", "b" * 64)
|
||||
for role, provider_id in ids.items()
|
||||
),
|
||||
queues=tuple(
|
||||
QueuePolicy(
|
||||
stage,
|
||||
capacity,
|
||||
min(80_000_000, terminal_timeout_ns),
|
||||
terminal_timeout_ns,
|
||||
)
|
||||
for stage in ("detector", "geometry", "temporal", "rolling", "threat")
|
||||
),
|
||||
authority=GraphAuthority(),
|
||||
)
|
||||
|
||||
|
||||
class _MemoryTelemetry:
|
||||
def __init__(self) -> None:
|
||||
self.records: list[dict[str, object]] = []
|
||||
@@ -358,6 +434,28 @@ def _graph(
|
||||
)
|
||||
|
||||
|
||||
def _graph_v2(
|
||||
source: object,
|
||||
*,
|
||||
detector: object | None = None,
|
||||
capacity: int = 8,
|
||||
run_mode: GraphRunMode = GraphRunMode.LOSSLESS_REPLAY,
|
||||
terminal_timeout_ns: int = 500_000_000,
|
||||
) -> ReferencePerceptionGraphV2:
|
||||
return ReferencePerceptionGraphV2(
|
||||
config=_config_v2(capacity, terminal_timeout_ns),
|
||||
source=source,
|
||||
detector=detector or _Detector(),
|
||||
geometry=_Geometry(),
|
||||
temporal=_Temporal(),
|
||||
motion=_Motion(),
|
||||
rolling=_Rolling(),
|
||||
threat=_Threat(),
|
||||
run_mode=run_mode,
|
||||
clock_ns=lambda: 10_000,
|
||||
)
|
||||
|
||||
|
||||
def test_reference_graph_closes_accounting_telemetry_and_deterministic_digest() -> None:
|
||||
telemetry = _MemoryTelemetry()
|
||||
graph = _graph(_Source((_packet(0), _packet(1))), telemetry=telemetry)
|
||||
@@ -386,6 +484,88 @@ def test_reference_graph_closes_accounting_telemetry_and_deterministic_digest()
|
||||
assert {"detector", "geometry", "temporal", "threat"}.issubset(stage_ids)
|
||||
|
||||
|
||||
def test_reference_graph_v2_publishes_current_and_retained_occupancy() -> None:
|
||||
result = _graph_v2(_Source((_packet(0), _packet(1)))).run()
|
||||
|
||||
assert result.graph_id == "reference-perception-graph/v2"
|
||||
assert result.run_mode is GraphRunMode.LOSSLESS_REPLAY
|
||||
assert [item.outcome for item in result.terminal_outcomes] == [
|
||||
TerminalOutcomeType.DELIVERED,
|
||||
TerminalOutcomeType.DELIVERED,
|
||||
]
|
||||
second = result.deliveries[1].obstacle_map
|
||||
assert [item.state for item in second.occupied] == [
|
||||
TemporalState.CURRENT,
|
||||
TemporalState.RETAINED,
|
||||
]
|
||||
assert second.unknown == ()
|
||||
assert set(dict(result.queue_high_watermarks)) == {
|
||||
"detector",
|
||||
"geometry",
|
||||
"temporal",
|
||||
"rolling",
|
||||
"threat",
|
||||
}
|
||||
|
||||
|
||||
def test_reference_graph_v2_lossless_mode_applies_bounded_backpressure() -> None:
|
||||
release = Event()
|
||||
|
||||
class BlockingDetector(_Detector):
|
||||
def detect(self, packet: SourcePacket) -> tuple[ObjectProposal2D, ...]:
|
||||
if packet.envelope.sequence == 0:
|
||||
assert release.wait(2)
|
||||
return super().detect(packet)
|
||||
|
||||
BlockingDetector.provider_id = _Detector.provider_id
|
||||
graph = _graph_v2(
|
||||
_Source(tuple(_packet(index) for index in range(4))),
|
||||
detector=BlockingDetector(),
|
||||
capacity=1,
|
||||
)
|
||||
holder: list[GraphRunResult] = []
|
||||
runner = threading.Thread(target=lambda: holder.append(graph.run()))
|
||||
runner.start()
|
||||
release.set()
|
||||
runner.join(3)
|
||||
|
||||
assert not runner.is_alive()
|
||||
assert len(holder[0].terminal_outcomes) == 4
|
||||
assert all(
|
||||
item.outcome is TerminalOutcomeType.DELIVERED
|
||||
for item in holder[0].terminal_outcomes
|
||||
)
|
||||
|
||||
|
||||
def test_reference_graph_v2_restart_requires_fresh_stateful_providers() -> None:
|
||||
graph = _graph_v2(_Source((_packet(0),)))
|
||||
|
||||
assert graph.run().state is GraphState.STOPPED
|
||||
with pytest.raises(
|
||||
GraphExecutionError,
|
||||
match="restart requires freshly instantiated providers",
|
||||
):
|
||||
graph.run()
|
||||
|
||||
|
||||
def test_reference_graph_v2_terminal_timeout_fails_stranded_packet() -> None:
|
||||
packet = _packet(0)
|
||||
graph = _graph_v2(
|
||||
_Source(()),
|
||||
capacity=1,
|
||||
terminal_timeout_ns=1,
|
||||
)
|
||||
queue = Queue(maxsize=1)
|
||||
queue.put_nowait(packet)
|
||||
|
||||
graph._put_stop(queue, "detector")
|
||||
|
||||
outcome = graph._outcomes[0]
|
||||
assert outcome.outcome is TerminalOutcomeType.FAILED
|
||||
assert outcome.reason == "terminal-queue-timeout"
|
||||
assert queue.qsize() == 1
|
||||
|
||||
|
||||
def test_reference_graph_marks_unavailable_stale_and_provider_failure() -> None:
|
||||
class FailingDetector(_Detector):
|
||||
def detect(self, packet: SourcePacket) -> tuple[ObjectProposal2D, ...]:
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.perception.contracts import LocalObstacleMap, SourceAccounting
|
||||
from k1link.perception.graph_contracts import (
|
||||
REFERENCE_GRAPH_ID_V2,
|
||||
DeliveredFrame,
|
||||
GraphRunMode,
|
||||
GraphState,
|
||||
TerminalOutcome,
|
||||
TerminalOutcomeType,
|
||||
build_graph_run_result_v2,
|
||||
)
|
||||
from k1link.perception.reference_graph_parity import (
|
||||
compare_reference_graph_to_accepted_ledgers,
|
||||
)
|
||||
|
||||
|
||||
def _result():
|
||||
obstacle_map = LocalObstacleMap(
|
||||
source_id="RAVNOVES00",
|
||||
session_id="20260720T065719Z_viewer_live",
|
||||
frame_id="frame-000000",
|
||||
graph_id=REFERENCE_GRAPH_ID_V2,
|
||||
generated_monotonic_ns=1,
|
||||
output_age_ns=0,
|
||||
occupied=(),
|
||||
unknown=(),
|
||||
camera_uncertainty=(),
|
||||
accounting=SourceAccounting(1, 1, 0, 0),
|
||||
)
|
||||
return build_graph_run_result_v2(
|
||||
graph_id=REFERENCE_GRAPH_ID_V2,
|
||||
source_profile_id="m4-ravnoves00-recorded-realtime/v1",
|
||||
run_mode=GraphRunMode.LOSSLESS_REPLAY,
|
||||
state=GraphState.STOPPED,
|
||||
admitted_count=1,
|
||||
outcomes=(
|
||||
TerminalOutcome(
|
||||
source_id="RAVNOVES00",
|
||||
session_id="20260720T065719Z_viewer_live",
|
||||
frame_id="frame-000000",
|
||||
sequence=0,
|
||||
outcome=TerminalOutcomeType.DELIVERED,
|
||||
stage_id="threat",
|
||||
reason="object-payload-delivered",
|
||||
),
|
||||
),
|
||||
deliveries=(DeliveredFrame(0, obstacle_map, ()),),
|
||||
queue_high_watermarks=(("detector", 1),),
|
||||
)
|
||||
|
||||
|
||||
def _write_ledgers(root: Path, *, threat_assessments: list[dict[str, object]]) -> tuple[Path, Path]:
|
||||
temporal = root / "temporal.jsonl"
|
||||
threat = root / "threat.jsonl"
|
||||
temporal.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"sequence": 0,
|
||||
"frame_id": "frame-000000",
|
||||
"current": [],
|
||||
"rolling_retained": [],
|
||||
"held": [],
|
||||
"expired": [],
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
"utf-8",
|
||||
)
|
||||
threat.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"sequence": 0,
|
||||
"frame_id": "frame-000000",
|
||||
"camera_proposals": [],
|
||||
"assessments": threat_assessments,
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
"utf-8",
|
||||
)
|
||||
return temporal, threat
|
||||
|
||||
|
||||
def test_reference_graph_parity_accepts_exact_frame_semantics(tmp_path: Path) -> None:
|
||||
temporal, threat = _write_ledgers(tmp_path, threat_assessments=[])
|
||||
|
||||
report = compare_reference_graph_to_accepted_ledgers(
|
||||
_result(),
|
||||
temporal_frames_path=temporal,
|
||||
threat_frames_path=threat,
|
||||
expected_frames=1,
|
||||
)
|
||||
|
||||
assert report.accepted is True
|
||||
assert dict(report.mismatch_counts) == {
|
||||
"camera_uncertainty": 0,
|
||||
"current": 0,
|
||||
"expired": 0,
|
||||
"held": 0,
|
||||
"rolling_retained": 0,
|
||||
"source_binding": 0,
|
||||
"threat_assessments": 0,
|
||||
}
|
||||
|
||||
|
||||
def test_reference_graph_parity_reports_threat_drift(tmp_path: Path) -> None:
|
||||
temporal, threat = _write_ledgers(tmp_path, threat_assessments=[{"component_id": "x"}])
|
||||
|
||||
report = compare_reference_graph_to_accepted_ledgers(
|
||||
_result(),
|
||||
temporal_frames_path=temporal,
|
||||
threat_frames_path=threat,
|
||||
expected_frames=1,
|
||||
)
|
||||
|
||||
assert report.accepted is False
|
||||
assert dict(report.mismatch_counts)["threat_assessments"] == 1
|
||||
@@ -0,0 +1,152 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from k1link.perception.contracts import LocalObstacleMap, SourceAccounting
|
||||
from k1link.perception.graph_contracts import (
|
||||
REFERENCE_GRAPH_ID_V2,
|
||||
DeliveredFrame,
|
||||
GraphRunMode,
|
||||
GraphState,
|
||||
TerminalOutcome,
|
||||
TerminalOutcomeType,
|
||||
build_graph_run_result_v2,
|
||||
)
|
||||
from k1link.perception.providers import ReferencePerceptionGraphConfigV2
|
||||
from k1link.perception.reference_graph_parity import ReferenceGraphParityReport
|
||||
from k1link.perception.reference_graph_result import seal_reference_graph_result
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _parity(accepted: bool = True) -> ReferenceGraphParityReport:
|
||||
return ReferenceGraphParityReport(
|
||||
expected_frames=1,
|
||||
compared_frames=1,
|
||||
temporal_frames_sha256="a" * 64,
|
||||
threat_frames_sha256="b" * 64,
|
||||
mismatch_counts=(("threat_assessments", 0 if accepted else 1),),
|
||||
accepted=accepted,
|
||||
)
|
||||
|
||||
|
||||
def _result(outcome: TerminalOutcomeType = TerminalOutcomeType.DELIVERED):
|
||||
terminal = TerminalOutcome(
|
||||
source_id="RAVNOVES00",
|
||||
session_id="20260720T065719Z_viewer_live",
|
||||
frame_id="frame-000000",
|
||||
sequence=0,
|
||||
outcome=outcome,
|
||||
stage_id="threat" if outcome is TerminalOutcomeType.DELIVERED else "detector",
|
||||
reason=(
|
||||
"object-payload-delivered"
|
||||
if outcome is TerminalOutcomeType.DELIVERED
|
||||
else "bounded-queue-latest-wins"
|
||||
),
|
||||
)
|
||||
deliveries = (
|
||||
(
|
||||
DeliveredFrame(
|
||||
sequence=0,
|
||||
obstacle_map=LocalObstacleMap(
|
||||
source_id="RAVNOVES00",
|
||||
session_id="20260720T065719Z_viewer_live",
|
||||
frame_id="frame-000000",
|
||||
graph_id=REFERENCE_GRAPH_ID_V2,
|
||||
generated_monotonic_ns=123,
|
||||
output_age_ns=45,
|
||||
occupied=(),
|
||||
unknown=(),
|
||||
camera_uncertainty=(),
|
||||
accounting=SourceAccounting(1, 1, 0, 0),
|
||||
),
|
||||
threats=(),
|
||||
),
|
||||
)
|
||||
if outcome is TerminalOutcomeType.DELIVERED
|
||||
else ()
|
||||
)
|
||||
return build_graph_run_result_v2(
|
||||
graph_id=REFERENCE_GRAPH_ID_V2,
|
||||
source_profile_id="m4-ravnoves00-recorded-realtime/v1",
|
||||
run_mode=GraphRunMode.LOSSLESS_REPLAY,
|
||||
state=GraphState.STOPPED,
|
||||
admitted_count=1,
|
||||
outcomes=(terminal,),
|
||||
deliveries=deliveries,
|
||||
queue_high_watermarks=(("detector", 1), ("threat", 1)),
|
||||
)
|
||||
|
||||
|
||||
def test_m47_graph_config_round_trips_and_pins_rolling_stage() -> None:
|
||||
path = REPOSITORY_ROOT / "config/perception/m4-reference-graph-v2.json"
|
||||
document = json.loads(path.read_text("utf-8"))
|
||||
config = ReferencePerceptionGraphConfigV2.from_dict(document)
|
||||
|
||||
assert config.to_dict() == document
|
||||
assert config.graph_id == REFERENCE_GRAPH_ID_V2
|
||||
assert {provider.role.value for provider in config.providers} == {
|
||||
"source",
|
||||
"detector",
|
||||
"geometry",
|
||||
"temporal",
|
||||
"motion",
|
||||
"rolling",
|
||||
"threat",
|
||||
}
|
||||
assert {queue.stage_id for queue in config.queues} == {
|
||||
"detector",
|
||||
"geometry",
|
||||
"temporal",
|
||||
"rolling",
|
||||
"threat",
|
||||
}
|
||||
|
||||
|
||||
def test_reference_graph_result_is_content_addressed_and_reproducible(tmp_path: Path) -> None:
|
||||
first = seal_reference_graph_result(
|
||||
_result(),
|
||||
output_root=tmp_path / "one",
|
||||
expected_frames=1,
|
||||
parity=_parity(),
|
||||
)
|
||||
second = seal_reference_graph_result(
|
||||
_result(),
|
||||
output_root=tmp_path / "two",
|
||||
expected_frames=1,
|
||||
parity=_parity(),
|
||||
)
|
||||
|
||||
assert first.accepted is True
|
||||
assert first.result_id == second.result_id
|
||||
assert first.report["gates"] == {
|
||||
"lossless_replay_mode": True,
|
||||
"graph_stopped": True,
|
||||
"admitted_frame_count": True,
|
||||
"terminal_accounting_closed": True,
|
||||
"delivery_count": True,
|
||||
"delivery_payload_count": True,
|
||||
"no_failed_frames": True,
|
||||
"no_stale_frames": True,
|
||||
"no_superseded_frames": True,
|
||||
"no_rejected_frames": True,
|
||||
"no_unavailable_frames": True,
|
||||
"accepted_m45r_m46_parity": True,
|
||||
}
|
||||
assert {
|
||||
path.name: path.read_bytes() for path in first.result_root.iterdir()
|
||||
} == {path.name: path.read_bytes() for path in second.result_root.iterdir()}
|
||||
|
||||
|
||||
def test_reference_graph_result_fails_closed_on_supersession(tmp_path: Path) -> None:
|
||||
sealed = seal_reference_graph_result(
|
||||
_result(TerminalOutcomeType.SUPERSEDED),
|
||||
output_root=tmp_path,
|
||||
expected_frames=1,
|
||||
parity=_parity(),
|
||||
)
|
||||
|
||||
assert sealed.accepted is False
|
||||
assert sealed.report["gates"]["no_superseded_frames"] is False
|
||||
assert sealed.report["gates"]["delivery_count"] is False
|
||||
Reference in New Issue
Block a user