fix(lab): стабилизировать нативный RAV004 replay
This commit is contained in:
@@ -143,6 +143,19 @@ interface RerunBlueprintChannel {
|
||||
};
|
||||
}
|
||||
|
||||
interface RerunNativeReceiver {
|
||||
endpointUrl: string;
|
||||
ready: () => boolean;
|
||||
open: (sourceUrl: string) => void;
|
||||
close: (sourceUrl: string) => void;
|
||||
}
|
||||
|
||||
interface LoadedNativePerceptionSource {
|
||||
receiver: RerunNativeReceiver;
|
||||
sourceUrl: string;
|
||||
byteLength: number;
|
||||
}
|
||||
|
||||
interface RecordedRerunIdentity {
|
||||
applicationId: "nodedc_mission_core_recorded";
|
||||
recordingId: string;
|
||||
@@ -557,6 +570,115 @@ export async function fetchRecordedPerceptionRrd(
|
||||
return payload;
|
||||
}
|
||||
|
||||
/** Bind a LAB sidecar to the exact immutable base recording generation. */
|
||||
export function resolveRecordedPerceptionViewerSourceUrl(
|
||||
endpointUrl: string,
|
||||
identity: RecordedRerunIdentity,
|
||||
baseGenerationSha256: string,
|
||||
origin: string,
|
||||
): string {
|
||||
const base = new URL(origin);
|
||||
const endpoint = new URL(endpointUrl, base.origin);
|
||||
if (
|
||||
endpoint.origin !== base.origin ||
|
||||
endpoint.search ||
|
||||
endpoint.hash ||
|
||||
!LAB_RECORDED_PERCEPTION_PATH.test(endpoint.pathname) ||
|
||||
identity.applicationId !== "nodedc_mission_core_recorded" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(identity.recordingId) ||
|
||||
!/^[a-f0-9]{64}$/.test(baseGenerationSha256)
|
||||
) {
|
||||
throw new Error("Unsafe recorded perception viewer source");
|
||||
}
|
||||
endpoint.searchParams.set("application_id", identity.applicationId);
|
||||
endpoint.searchParams.set("recording_id", identity.recordingId);
|
||||
endpoint.searchParams.set("generation", baseGenerationSha256);
|
||||
return endpoint.href;
|
||||
}
|
||||
|
||||
/** Verify the immutable URL with a four-byte range request before Rerun opens it. */
|
||||
export async function probeRecordedPerceptionViewerSource(
|
||||
sourceUrl: string,
|
||||
{
|
||||
origin,
|
||||
signal,
|
||||
fetcher = globalThis.fetch,
|
||||
}: {
|
||||
origin: string;
|
||||
signal?: AbortSignal;
|
||||
fetcher?: typeof globalThis.fetch;
|
||||
},
|
||||
): Promise<number> {
|
||||
const base = new URL(origin);
|
||||
const endpoint = new URL(sourceUrl, base.origin);
|
||||
const allowedParameters = ["application_id", "generation", "recording_id"];
|
||||
if (
|
||||
endpoint.origin !== base.origin ||
|
||||
endpoint.hash ||
|
||||
!LAB_RECORDED_PERCEPTION_PATH.test(endpoint.pathname) ||
|
||||
[...endpoint.searchParams.keys()].sort().join("\0") !== allowedParameters.join("\0") ||
|
||||
endpoint.searchParams.get("application_id") !== "nodedc_mission_core_recorded" ||
|
||||
!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(
|
||||
endpoint.searchParams.get("recording_id") ?? "",
|
||||
) ||
|
||||
!/^[a-f0-9]{64}$/.test(endpoint.searchParams.get("generation") ?? "")
|
||||
) {
|
||||
throw new Error("Unsafe recorded perception viewer source");
|
||||
}
|
||||
const response = await fetcher(endpoint.href, {
|
||||
method: "GET",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
Accept: "application/vnd.rerun.rrd",
|
||||
Range: "bytes=0-3",
|
||||
},
|
||||
signal,
|
||||
});
|
||||
const contentType = response.headers.get("Content-Type")?.split(";", 1)[0].trim();
|
||||
const contentRange = response.headers.get("Content-Range");
|
||||
const rangedLength = contentRange?.match(/^bytes 0-3\/(\d+)$/)?.[1];
|
||||
const declaredLength = Number(
|
||||
rangedLength ?? response.headers.get("Content-Length"),
|
||||
);
|
||||
if (
|
||||
![200, 206].includes(response.status) ||
|
||||
contentType !== "application/vnd.rerun.rrd" ||
|
||||
!Number.isSafeInteger(declaredLength) ||
|
||||
declaredLength < 4 ||
|
||||
declaredLength > MAX_PERCEPTION_BYTES
|
||||
) {
|
||||
throw new Error("Invalid recorded perception viewer response");
|
||||
}
|
||||
const prefix = new Uint8Array(4);
|
||||
let receivedBytes = 0;
|
||||
const reader = response.body?.getReader();
|
||||
if (reader) {
|
||||
while (receivedBytes < prefix.byteLength) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const count = Math.min(value.byteLength, prefix.byteLength - receivedBytes);
|
||||
prefix.set(value.subarray(0, count), receivedBytes);
|
||||
receivedBytes += count;
|
||||
}
|
||||
await reader.cancel();
|
||||
} else {
|
||||
const payload = new Uint8Array(await response.arrayBuffer());
|
||||
const count = Math.min(payload.byteLength, prefix.byteLength);
|
||||
prefix.set(payload.subarray(0, count));
|
||||
receivedBytes = count;
|
||||
}
|
||||
if (
|
||||
receivedBytes !== prefix.byteLength ||
|
||||
prefix[0] !== 0x52 ||
|
||||
prefix[1] !== 0x52 ||
|
||||
prefix[2] !== 0x46 ||
|
||||
prefix[3] !== 0x32
|
||||
) {
|
||||
throw new Error("Invalid recorded perception viewer RRD");
|
||||
}
|
||||
return declaredLength;
|
||||
}
|
||||
|
||||
export function RerunViewport({
|
||||
profile,
|
||||
onStatusChange,
|
||||
@@ -617,7 +739,9 @@ export function RerunViewport({
|
||||
const uiBuildStaleRef = useRef(false);
|
||||
const blueprintChannelRef = useRef<RerunBlueprintChannel | null>(null);
|
||||
const perceptionChannelRef = useRef<RerunBlueprintChannel | null>(null);
|
||||
const perceptionReceiverRef = useRef<RerunNativeReceiver | null>(null);
|
||||
const loadedPerceptionChannelRef = useRef<RerunBlueprintChannel | null>(null);
|
||||
const loadedNativePerceptionSourceRef = useRef<LoadedNativePerceptionSource | null>(null);
|
||||
const appliedPointColorKeyRef = useRef<string | null>(null);
|
||||
const recordedIdentityRef = useRef<RecordedRerunIdentity | null>(null);
|
||||
const blueprintSessionIdRef = useRef(crypto.randomUUID().replaceAll("-", ""));
|
||||
@@ -733,6 +857,7 @@ export function RerunViewport({
|
||||
const recordedAutoplay = createRecordedAutoplayGate();
|
||||
let blueprintChannel: RerunBlueprintChannel | null = null;
|
||||
let perceptionChannel: RerunBlueprintChannel | null = null;
|
||||
let perceptionReceiver: RerunNativeReceiver | null = null;
|
||||
const unsubscribers: Array<() => void> = [];
|
||||
const unsubscribeAll = () => {
|
||||
while (unsubscribers.length > 0) {
|
||||
@@ -989,6 +1114,9 @@ export function RerunViewport({
|
||||
if (perceptionChannelRef.current === perceptionChannel) {
|
||||
perceptionChannelRef.current = null;
|
||||
}
|
||||
if (perceptionReceiverRef.current === perceptionReceiver) {
|
||||
perceptionReceiverRef.current = null;
|
||||
}
|
||||
recordedIdentityRef.current = null;
|
||||
try {
|
||||
blueprintChannel?.channel.close();
|
||||
@@ -1000,6 +1128,15 @@ export function RerunViewport({
|
||||
} catch {
|
||||
// The viewer may already have closed all auxiliary channels.
|
||||
}
|
||||
const loadedNativeSource = loadedNativePerceptionSourceRef.current;
|
||||
if (loadedNativeSource?.receiver === perceptionReceiver) {
|
||||
loadedNativePerceptionSourceRef.current = null;
|
||||
try {
|
||||
perceptionReceiver?.close(loadedNativeSource.sourceUrl);
|
||||
} catch {
|
||||
// The viewer may already have closed all native receivers.
|
||||
}
|
||||
}
|
||||
}, () => {
|
||||
try {
|
||||
if (viewer.ready) viewer.close(resolvedSource);
|
||||
@@ -1363,9 +1500,19 @@ export function RerunViewport({
|
||||
setBlueprintChannelRevision((revision) => revision + 1);
|
||||
}
|
||||
if (recordedPerceptionUrl) {
|
||||
const channel = viewer.open_channel("missioncore/recorded-perception");
|
||||
perceptionChannel = { endpointUrl: recordedPerceptionUrl, channel };
|
||||
perceptionChannelRef.current = perceptionChannel;
|
||||
if (LAB_RECORDED_PERCEPTION_PATH.test(new URL(recordedPerceptionUrl).pathname)) {
|
||||
perceptionReceiver = {
|
||||
endpointUrl: recordedPerceptionUrl,
|
||||
ready: () => viewer.ready,
|
||||
open: (source) => viewer.open(source),
|
||||
close: (source) => viewer.close(source),
|
||||
};
|
||||
perceptionReceiverRef.current = perceptionReceiver;
|
||||
} else {
|
||||
const channel = viewer.open_channel("missioncore/recorded-perception");
|
||||
perceptionChannel = { endpointUrl: recordedPerceptionUrl, channel };
|
||||
perceptionChannelRef.current = perceptionChannel;
|
||||
}
|
||||
setPerceptionChannelRevision((revision) => revision + 1);
|
||||
}
|
||||
|
||||
@@ -1440,6 +1587,7 @@ export function RerunViewport({
|
||||
|
||||
useEffect(() => {
|
||||
loadedPerceptionChannelRef.current = null;
|
||||
loadedNativePerceptionSourceRef.current = null;
|
||||
}, [recordedPerceptionUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1459,11 +1607,91 @@ export function RerunViewport({
|
||||
}
|
||||
return;
|
||||
}
|
||||
const active = perceptionChannelRef.current;
|
||||
const identity = recordedIdentityRef.current;
|
||||
if (!identity) return;
|
||||
if (LAB_RECORDED_PERCEPTION_PATH.test(new URL(recordedPerceptionUrl).pathname)) {
|
||||
const receiver = perceptionReceiverRef.current;
|
||||
if (
|
||||
!receiver ||
|
||||
receiver.endpointUrl !== recordedPerceptionUrl ||
|
||||
!receiver.ready() ||
|
||||
!recordedArtifact
|
||||
) return;
|
||||
let sourceUrl: string;
|
||||
try {
|
||||
sourceUrl = resolveRecordedPerceptionViewerSourceUrl(
|
||||
recordedPerceptionUrl,
|
||||
identity,
|
||||
recordedArtifact.sha256,
|
||||
window.location.origin,
|
||||
);
|
||||
} catch {
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "error",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "AI-слои не привязаны к поколению записи.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const loaded = loadedNativePerceptionSourceRef.current;
|
||||
if (loaded?.receiver === receiver && loaded.sourceUrl === sourceUrl) {
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "ready",
|
||||
receivedBytes: loaded.byteLength,
|
||||
totalBytes: loaded.byteLength,
|
||||
progress: 1,
|
||||
message: "AI-слои подключены к Rerun.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const abort = new AbortController();
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "loading",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "Подключаем AI-слои к Rerun.",
|
||||
});
|
||||
void probeRecordedPerceptionViewerSource(sourceUrl, {
|
||||
origin: window.location.origin,
|
||||
signal: abort.signal,
|
||||
}).then((byteLength) => {
|
||||
if (
|
||||
abort.signal.aborted ||
|
||||
perceptionReceiverRef.current !== receiver ||
|
||||
recordedIdentityRef.current !== identity ||
|
||||
!receiver.ready()
|
||||
) return;
|
||||
receiver.open(sourceUrl);
|
||||
loadedNativePerceptionSourceRef.current = {
|
||||
receiver,
|
||||
sourceUrl,
|
||||
byteLength,
|
||||
};
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "ready",
|
||||
receivedBytes: byteLength,
|
||||
totalBytes: byteLength,
|
||||
progress: 1,
|
||||
message: "AI-слои подключены к Rerun.",
|
||||
});
|
||||
}).catch(() => {
|
||||
if (abort.signal.aborted) return;
|
||||
onPerceptionLoadChange?.({
|
||||
phase: "error",
|
||||
receivedBytes: 0,
|
||||
totalBytes: null,
|
||||
progress: null,
|
||||
message: "AI-слои не загрузились. Можно повторить.",
|
||||
});
|
||||
});
|
||||
return () => abort.abort();
|
||||
}
|
||||
const active = perceptionChannelRef.current;
|
||||
if (
|
||||
!active ||
|
||||
!identity ||
|
||||
active.endpointUrl !== recordedPerceptionUrl ||
|
||||
!active.channel.ready
|
||||
) return;
|
||||
@@ -1604,6 +1832,7 @@ export function RerunViewport({
|
||||
}, [
|
||||
onPerceptionLoadChange,
|
||||
perceptionChannelRevision,
|
||||
recordedArtifact?.sha256,
|
||||
recordedPerceptionLayers.enabled,
|
||||
recordedPerceptionRetryGeneration,
|
||||
recordedPerceptionUrl,
|
||||
|
||||
@@ -139,6 +139,7 @@ export function CanonicalRecordedLabReplay<
|
||||
value={mediaMode}
|
||||
items={[...mediaModes]}
|
||||
label="Видео и камера"
|
||||
size="dense"
|
||||
onChange={onMediaModeChange}
|
||||
/>
|
||||
</div>
|
||||
@@ -149,6 +150,7 @@ export function CanonicalRecordedLabReplay<
|
||||
value={spatialMode}
|
||||
items={[...spatialModes]}
|
||||
label="3D и план"
|
||||
size="dense"
|
||||
onChange={onSpatialModeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -90,7 +90,7 @@ export function CanonicalVegetationRerunReplay({
|
||||
showPoints: spatialMode !== null,
|
||||
showTrajectory: spatialMode !== null,
|
||||
showGrid: spatialMode !== null,
|
||||
pointSize: 2.2,
|
||||
pointSize: 3.8,
|
||||
}), [spatialLayer, spatialMode]);
|
||||
const profile = launch ? recordedSessionRerunProfile({
|
||||
sourceUrl: launch.sourceUrl,
|
||||
@@ -107,7 +107,7 @@ export function CanonicalVegetationRerunReplay({
|
||||
initialPlaybackStartSeconds: review.timelineStartSeconds,
|
||||
view: mediaMode !== null ? "perception" : "spatial",
|
||||
viewResetGeneration,
|
||||
followTrajectory: false,
|
||||
followTrajectory: true,
|
||||
perceptionSourceUrl:
|
||||
`/api/v1/laboratory/vegetation-shadow/${encodeURIComponent(resultId)}` +
|
||||
"/canonical-overlay.rrd",
|
||||
@@ -131,7 +131,7 @@ export function CanonicalVegetationRerunReplay({
|
||||
aria-label="Слои камеры и видео"
|
||||
>
|
||||
<Button
|
||||
size="compact"
|
||||
size="dense"
|
||||
shape="pill"
|
||||
variant={showSemantics ? "primary" : "secondary"}
|
||||
aria-pressed={showSemantics}
|
||||
@@ -146,6 +146,7 @@ export function CanonicalVegetationRerunReplay({
|
||||
{ value: "vegetation", label: "ПРИРОДА · DDRNet" },
|
||||
]}
|
||||
label="Источник семантики"
|
||||
size="dense"
|
||||
onChange={(value) => {
|
||||
setSemanticLayer(value);
|
||||
setShowSemantics(true);
|
||||
@@ -168,13 +169,14 @@ export function CanonicalVegetationRerunReplay({
|
||||
{ value: "semantic", label: "SEMANTICS", disabled: true },
|
||||
]}
|
||||
label="Пространственные слои"
|
||||
size="dense"
|
||||
onChange={setSpatialLayer}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const resetSpatialView = (
|
||||
<Button
|
||||
size="compact"
|
||||
size="dense"
|
||||
variant="ghost"
|
||||
icon={<Icon name="refresh" size={14} />}
|
||||
aria-label="Сбросить положение 3D камеры"
|
||||
|
||||
@@ -42,6 +42,8 @@ let resolveRecordedBlueprintUrl;
|
||||
let fetchRecordedBlueprintRrd;
|
||||
let resolveRecordedPerceptionUrl;
|
||||
let fetchRecordedPerceptionRrd;
|
||||
let resolveRecordedPerceptionViewerSourceUrl;
|
||||
let probeRecordedPerceptionViewerSource;
|
||||
let resolveRecordedPointColorsUrl;
|
||||
let fetchRecordedPointColorsRrd;
|
||||
let recordedPointColorKey;
|
||||
@@ -110,6 +112,8 @@ before(async () => {
|
||||
fetchRecordedBlueprintRrd,
|
||||
resolveRecordedPerceptionUrl,
|
||||
fetchRecordedPerceptionRrd,
|
||||
resolveRecordedPerceptionViewerSourceUrl,
|
||||
probeRecordedPerceptionViewerSource,
|
||||
resolveRecordedPointColorsUrl,
|
||||
fetchRecordedPointColorsRrd,
|
||||
recordedPointColorKey,
|
||||
@@ -666,6 +670,41 @@ test("recorded perception fetch admits one complete same-origin RRD or no layer"
|
||||
assert.equal(absent, null);
|
||||
});
|
||||
|
||||
test("LAB perception sidecar is streamed by native Rerun from one generation-bound URL", async () => {
|
||||
const endpoint =
|
||||
`http://127.0.0.1:5174/api/v1/laboratory/vegetation-shadow/` +
|
||||
`lab-v1-vegetation-shadow-${"a".repeat(64)}/canonical-overlay.rrd`;
|
||||
const sourceUrl = resolveRecordedPerceptionViewerSourceUrl(
|
||||
endpoint,
|
||||
{ applicationId: "nodedc_mission_core_recorded", recordingId: "recording-001" },
|
||||
"b".repeat(64),
|
||||
"http://127.0.0.1:5174",
|
||||
);
|
||||
assert.equal(
|
||||
sourceUrl,
|
||||
`${endpoint}?application_id=nodedc_mission_core_recorded` +
|
||||
`&recording_id=recording-001&generation=${"b".repeat(64)}`,
|
||||
);
|
||||
|
||||
const byteLength = await probeRecordedPerceptionViewerSource(sourceUrl, {
|
||||
origin: "http://127.0.0.1:5174",
|
||||
fetcher: async (input, init) => {
|
||||
assert.equal(String(input), sourceUrl);
|
||||
assert.equal(init.method, "GET");
|
||||
assert.equal(new Headers(init.headers).get("Range"), "bytes=0-3");
|
||||
return new Response(Uint8Array.from([0x52, 0x52, 0x46, 0x32]), {
|
||||
status: 206,
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.rerun.rrd",
|
||||
"Content-Length": "4",
|
||||
"Content-Range": "bytes 0-3/393203594",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
assert.equal(byteLength, 393_203_594);
|
||||
});
|
||||
|
||||
test("recorded replay creates an isolated source catalog without live device bindings", () => {
|
||||
const sources = recordedObservationSources({
|
||||
kind: "rerun-recording",
|
||||
|
||||
@@ -93,6 +93,7 @@ class _RecordedBlueprintStream:
|
||||
eye_contract = (follow_trajectory, plan_view)
|
||||
update_eye_controls = self._eye_contract != eye_contract
|
||||
blueprint = blueprint_factory(update_eye_controls)
|
||||
make_active = self._sequence == 0
|
||||
self._blueprint_recording.set_time(
|
||||
"blueprint",
|
||||
sequence=self._sequence,
|
||||
@@ -102,7 +103,7 @@ class _RecordedBlueprintStream:
|
||||
self._blueprint_recording.flush(timeout_sec=5.0)
|
||||
bindings.send_blueprint(
|
||||
self._blueprint_memory.storage,
|
||||
True,
|
||||
make_active,
|
||||
False,
|
||||
self._transport_recording.to_native(),
|
||||
)
|
||||
|
||||
@@ -12,10 +12,10 @@ import zipfile
|
||||
from collections.abc import Callable
|
||||
from functools import lru_cache
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Any, Final, Literal
|
||||
from typing import Annotated, Any, Final, Literal
|
||||
|
||||
import numpy as np
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from fastapi.responses import FileResponse, JSONResponse, Response
|
||||
from PIL import Image
|
||||
@@ -298,10 +298,11 @@ def _build_vegetation_lab_router(
|
||||
},
|
||||
)
|
||||
|
||||
@router.post("/{result_id}/canonical-overlay.rrd")
|
||||
async def get_canonical_rerun_overlay(
|
||||
async def canonical_rerun_overlay_response(
|
||||
result_id: str,
|
||||
request: CanonicalLabRerunRequest,
|
||||
*,
|
||||
expected_base_generation_sha256: str | None = None,
|
||||
) -> FileResponse:
|
||||
"""Project LAB-only evidence into the base recording's native clock."""
|
||||
|
||||
@@ -319,6 +320,11 @@ def _build_vegetation_lab_router(
|
||||
if recording is None:
|
||||
raise HTTPException(status_code=409, detail="Canonical spatial recording is not ready")
|
||||
recording_path, generation_sha256 = recording
|
||||
if (
|
||||
expected_base_generation_sha256 is not None
|
||||
and expected_base_generation_sha256 != generation_sha256
|
||||
):
|
||||
raise HTTPException(status_code=412, detail="Canonical recording generation changed")
|
||||
try:
|
||||
expected_recording_id = await run_in_threadpool(
|
||||
canonical_recording_id,
|
||||
@@ -353,6 +359,43 @@ def _build_vegetation_lab_router(
|
||||
},
|
||||
)
|
||||
|
||||
@router.post("/{result_id}/canonical-overlay.rrd")
|
||||
async def get_canonical_rerun_overlay(
|
||||
result_id: str,
|
||||
request: CanonicalLabRerunRequest,
|
||||
) -> FileResponse:
|
||||
"""Resolve the sealed sidecar for bounded non-viewer consumers."""
|
||||
|
||||
return await canonical_rerun_overlay_response(result_id, request)
|
||||
|
||||
@router.get("/{result_id}/canonical-overlay.rrd")
|
||||
async def stream_canonical_rerun_overlay(
|
||||
result_id: str,
|
||||
application_id: Annotated[
|
||||
Literal["nodedc_mission_core_recorded"],
|
||||
Query(),
|
||||
],
|
||||
recording_id: Annotated[
|
||||
str,
|
||||
Query(
|
||||
min_length=1,
|
||||
max_length=128,
|
||||
pattern=r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$",
|
||||
),
|
||||
],
|
||||
generation: Annotated[str, Query(pattern=r"^[a-f0-9]{64}$")],
|
||||
) -> FileResponse:
|
||||
"""Stream one immutable LAB sidecar through Rerun's native HTTP receiver."""
|
||||
|
||||
return await canonical_rerun_overlay_response(
|
||||
result_id,
|
||||
CanonicalLabRerunRequest(
|
||||
application_id=application_id,
|
||||
recording_id=recording_id,
|
||||
),
|
||||
expected_base_generation_sha256=generation,
|
||||
)
|
||||
|
||||
@router.get("/{result_id}/timeline")
|
||||
def get_canonical_route_timeline(result_id: str) -> dict[str, object]:
|
||||
candidate = _resolve_candidate(root_provider, definition, result_id)
|
||||
|
||||
@@ -594,9 +594,9 @@ def test_recorded_blueprint_layer_updates_preserve_store_until_explicit_reset(
|
||||
assert eye_control_updates == [True, False, True, False, True]
|
||||
assert [activation[1:] for activation in activations] == [
|
||||
(True, False),
|
||||
(True, False),
|
||||
(True, False),
|
||||
(True, False),
|
||||
(False, False),
|
||||
(False, False),
|
||||
(False, False),
|
||||
(True, False),
|
||||
]
|
||||
assert activations[0][0] is activations[1][0]
|
||||
|
||||
@@ -17,6 +17,7 @@ from PIL import Image
|
||||
import k1link.laboratory.vegetation_policy_review as policy_review_module
|
||||
import k1link.laboratory.vegetation_policy_video as policy_video_module
|
||||
import k1link.laboratory.vegetation_shadow_lab as vegetation_lab_module
|
||||
import k1link.web.vegetation_shadow_lab_api as vegetation_api_module
|
||||
from k1link.laboratory import LaboratoryEvidenceRegistry
|
||||
from k1link.laboratory.canonical_rerun_overlay import (
|
||||
CanonicalLabOverlayArtifact,
|
||||
@@ -89,6 +90,90 @@ def test_canonical_overlay_memory_cache_rejects_same_size_tampering(
|
||||
assert not _artifact_is_regular(artifact)
|
||||
|
||||
|
||||
def test_canonical_overlay_get_is_generation_bound_and_range_streamable(
|
||||
tmp_path: Path,
|
||||
monkeypatch,
|
||||
) -> None:
|
||||
result_id = f"lab-v1-vegetation-shadow-{'a' * 64}"
|
||||
result_root = tmp_path / result_id
|
||||
result_root.mkdir()
|
||||
base = tmp_path / "base.rrd"
|
||||
base.write_bytes(b"RRF2-base")
|
||||
overlay = tmp_path / "overlay.rrd"
|
||||
overlay.write_bytes(b"RRF2-overlay")
|
||||
generation = "b" * 64
|
||||
recording_id = "recording-001"
|
||||
artifact = CanonicalLabOverlayArtifact(
|
||||
path=overlay,
|
||||
byte_length=overlay.stat().st_size,
|
||||
sha256=_sha256(overlay),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"_resolve_candidate",
|
||||
lambda *_args, **_kwargs: result_root,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"_read_verified",
|
||||
lambda *_args, **_kwargs: {},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"_full_route_context",
|
||||
lambda *_args, **_kwargs: ({"session_id": "session-001"}, ()),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"canonical_recording_id",
|
||||
lambda _path: recording_id,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
vegetation_api_module,
|
||||
"canonical_lab_overlay",
|
||||
lambda *_args, **_kwargs: artifact,
|
||||
)
|
||||
ffmpeg = tmp_path / "ffmpeg"
|
||||
ffmpeg.write_bytes(b"fixture")
|
||||
ffmpeg.chmod(0o700)
|
||||
app = FastAPI()
|
||||
app.include_router(
|
||||
build_vegetation_shadow_lab_router(
|
||||
root_provider=lambda: tmp_path,
|
||||
canonical_recording_provider=lambda _session_id: (base, generation),
|
||||
jobs_root=tmp_path,
|
||||
rerun_overlay_cache_root=tmp_path / "cache",
|
||||
ffmpeg_path=ffmpeg,
|
||||
)
|
||||
)
|
||||
client = TestClient(app)
|
||||
endpoint = f"/api/v1/laboratory/vegetation-shadow/{result_id}/canonical-overlay.rrd"
|
||||
response = client.get(
|
||||
endpoint,
|
||||
params={
|
||||
"application_id": "nodedc_mission_core_recorded",
|
||||
"recording_id": recording_id,
|
||||
"generation": generation,
|
||||
},
|
||||
headers={"Range": "bytes=0-3"},
|
||||
)
|
||||
assert response.status_code == 206
|
||||
assert response.content == b"RRF2"
|
||||
assert response.headers["content-range"] == f"bytes 0-3/{artifact.byte_length}"
|
||||
assert response.headers["etag"] == f'"{artifact.sha256}"'
|
||||
assert response.headers["cache-control"].endswith("immutable")
|
||||
|
||||
stale = client.get(
|
||||
endpoint,
|
||||
params={
|
||||
"application_id": "nodedc_mission_core_recorded",
|
||||
"recording_id": recording_id,
|
||||
"generation": "c" * 64,
|
||||
},
|
||||
)
|
||||
assert stale.status_code == 412
|
||||
|
||||
|
||||
def test_route_playback_chunk_descriptor_seals_only_requested_binary_window() -> None:
|
||||
points = np.arange(18, dtype="<f4").reshape(6, 3)
|
||||
descriptor = _canonical_route_playback_chunk_descriptor(
|
||||
|
||||
Reference in New Issue
Block a user