416 lines
14 KiB
JavaScript
416 lines
14 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { after, before, test } from "node:test";
|
|
|
|
import { createServer } from "vite";
|
|
|
|
let server;
|
|
let xgridsK1Manifest;
|
|
let xgridsK1ObservationSources;
|
|
let openObservationSource;
|
|
let shouldRestartObservationSource;
|
|
let consumeCameraLeaseRetry;
|
|
let resetCameraLeaseRetryBudget;
|
|
let initialObservationWindowRect;
|
|
|
|
before(async () => {
|
|
server = await createServer({
|
|
appType: "custom",
|
|
logLevel: "silent",
|
|
server: { middlewareMode: true },
|
|
});
|
|
({ xgridsK1Manifest } = await server.ssrLoadModule(
|
|
"/src/device-plugins/xgrids-k1/manifest.ts",
|
|
));
|
|
({ xgridsK1ObservationSources } = await server.ssrLoadModule(
|
|
"/src/device-plugins/xgrids-k1/observationSources.ts",
|
|
));
|
|
({ openObservationSource, shouldRestartObservationSource } = await server.ssrLoadModule(
|
|
"/src/core/observation/layoutPolicy.ts",
|
|
));
|
|
({ consumeCameraLeaseRetry, resetCameraLeaseRetryBudget } = await server.ssrLoadModule(
|
|
"/src/components/MseFmp4WebSocketPlayer.tsx",
|
|
));
|
|
({ initialObservationWindowRect } = await server.ssrLoadModule(
|
|
"/src/components/FloatingObservationWindow.tsx",
|
|
));
|
|
});
|
|
|
|
test("observation camera windows tile from the bottom-right above the live timeline", () => {
|
|
const bounds = { width: 1280, height: 720 };
|
|
const left = initialObservationWindowRect(0, 2, bounds);
|
|
const right = initialObservationWindowRect(1, 2, bounds);
|
|
|
|
assert.equal(left.y, right.y);
|
|
assert.ok(left.x + left.width < right.x);
|
|
assert.equal(right.x + right.width, bounds.width - 18);
|
|
assert.ok(right.y + right.height <= bounds.height - 64);
|
|
});
|
|
|
|
test("observation camera windows stack without overlap when the viewport is narrow", () => {
|
|
const bounds = { width: 420, height: 700 };
|
|
const lower = initialObservationWindowRect(0, 2, bounds);
|
|
const upper = initialObservationWindowRect(1, 2, bounds);
|
|
|
|
assert.equal(lower.x, upper.x);
|
|
assert.ok(upper.y + upper.height < lower.y);
|
|
for (const rect of [lower, upper]) {
|
|
assert.ok(rect.x >= 0 && rect.y >= 0);
|
|
assert.ok(rect.x + rect.width <= bounds.width);
|
|
assert.ok(rect.y + rect.height <= bounds.height - 64);
|
|
}
|
|
});
|
|
|
|
test("observation camera tiling remains in bounds on a constrained viewport", () => {
|
|
const bounds = { width: 260, height: 280 };
|
|
const rects = Array.from({ length: 2 }, (_, index) => (
|
|
initialObservationWindowRect(index, 2, bounds)
|
|
));
|
|
|
|
for (const rect of rects) {
|
|
assert.ok(rect.width > 0 && rect.height > 0);
|
|
assert.ok(rect.x >= 0 && rect.y >= 0);
|
|
assert.ok(rect.x + rect.width <= bounds.width);
|
|
assert.ok(rect.y + rect.height <= bounds.height - 64);
|
|
}
|
|
assert.ok(rects[1].y + rects[1].height <= rects[0].y);
|
|
});
|
|
|
|
after(async () => {
|
|
await server?.close();
|
|
});
|
|
|
|
function model() {
|
|
const activeModel = xgridsK1Manifest.spec.models[0];
|
|
assert.ok(activeModel, "XGRIDS plugin must declare at least one model");
|
|
return activeModel;
|
|
}
|
|
|
|
function cameraRow(sourceId, label) {
|
|
return {
|
|
stream_id: `camera.preview.${sourceId.split(".").at(-1)}`,
|
|
source_id: sourceId,
|
|
semantic_channel_id: "camera.preview.live",
|
|
label,
|
|
sensor_kind: "camera",
|
|
modality: "encoded-video",
|
|
availability: "available",
|
|
endpoint_label: "MSE · fMP4",
|
|
activation: {
|
|
group_id: "camera.preview.decoder",
|
|
max_active: 1,
|
|
selected: false,
|
|
controllable: true,
|
|
},
|
|
delivery: null,
|
|
};
|
|
}
|
|
|
|
function declaredState(cameraRows = [
|
|
cameraRow("sensor.camera.left", "K1 · камера слева"),
|
|
cameraRow("sensor.camera.right", "K1 · камера справа"),
|
|
]) {
|
|
const activeModel = model();
|
|
return {
|
|
phase: "connected",
|
|
source_mode: "idle",
|
|
k1_ip: "192.168.7.10",
|
|
foxglove_ws_url: "ws://192.168.7.10:8765",
|
|
foxglove_viewer_url: "http://192.168.7.10:8765/vendor-viewer",
|
|
compatibility: {
|
|
profile_id: "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
|
|
camera_preview: "rtsp://192.168.7.10:8554/vendor-preview",
|
|
},
|
|
device_ref: {
|
|
device_id: "device-k1-001",
|
|
model_id: activeModel.id,
|
|
identity_stability: "stable",
|
|
identity_basis: "hardware-identifier",
|
|
},
|
|
device_session: {
|
|
device_session_id: "device-session-001",
|
|
device_id: "device-k1-001",
|
|
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
|
|
connectivity: "connected",
|
|
},
|
|
sensor_catalog: {
|
|
schema_version: "missioncore.sensor-catalog/v1alpha2",
|
|
revision: "test-profile",
|
|
streams: [
|
|
{ stream_id: "spatial.point-cloud.live", modality: "point-cloud", availability: "observed" },
|
|
...cameraRows,
|
|
],
|
|
},
|
|
camera_preview: {
|
|
phase: "idle",
|
|
revision: 1,
|
|
generation: 0,
|
|
active_source_id: null,
|
|
delivery: null,
|
|
},
|
|
};
|
|
}
|
|
|
|
function pointCloudStreamingState() {
|
|
return {
|
|
...declaredState(),
|
|
phase: "streaming",
|
|
source_mode: "live",
|
|
rerun_grpc_url: "rerun+http://127.0.0.1:9877/proxy",
|
|
acquisition: {
|
|
acquisition_id: "acquisition-001",
|
|
device_id: "device-k1-001",
|
|
device_session_id: "device-session-001",
|
|
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
|
|
control_mode: "operator-manual",
|
|
requested_streams: ["spatial.point-cloud.live"],
|
|
target_host: "127.0.0.1",
|
|
duration_seconds: 0,
|
|
evidence_policy: "required",
|
|
state: "acquiring",
|
|
state_revision: 3,
|
|
},
|
|
};
|
|
}
|
|
|
|
function cameraStreamingState(sourceId) {
|
|
const state = pointCloudStreamingState();
|
|
const delivery = {
|
|
id: `preview-generation-7:${sourceId}`,
|
|
kind: "mse-fmp4-websocket",
|
|
url: "/api/v1/device-plugins/xgrids-k1/camera-preview/ws?generation=7",
|
|
media_type: 'video/mp4; codecs="avc1.640028"',
|
|
};
|
|
state.sensor_catalog = {
|
|
...state.sensor_catalog,
|
|
streams: state.sensor_catalog.streams.map((stream) =>
|
|
stream.source_id === sourceId
|
|
? {
|
|
...stream,
|
|
availability: "streaming",
|
|
activation: { ...stream.activation, selected: true },
|
|
delivery,
|
|
}
|
|
: stream),
|
|
};
|
|
state.camera_preview = {
|
|
phase: "streaming",
|
|
revision: 4,
|
|
generation: 7,
|
|
active_source_id: sourceId,
|
|
delivery,
|
|
};
|
|
return state;
|
|
}
|
|
|
|
function collectUrlLikeStrings(value, found = []) {
|
|
if (typeof value === "string") {
|
|
if (value.includes("://")) found.push(value);
|
|
return found;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
for (const item of value) collectUrlLikeStrings(item, found);
|
|
return found;
|
|
}
|
|
if (value && typeof value === "object") {
|
|
for (const item of Object.values(value)) collectUrlLikeStrings(item, found);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
test("K1 maps zero, one or N catalog cameras without model-specific source ids", () => {
|
|
const zero = xgridsK1ObservationSources(declaredState([]), model());
|
|
const one = xgridsK1ObservationSources(
|
|
declaredState([cameraRow("rig.front", "Передняя камера")]),
|
|
model(),
|
|
);
|
|
const many = xgridsK1ObservationSources(declaredState(), model());
|
|
|
|
assert.deepEqual(zero.map(({ modality }) => modality), ["point-cloud"]);
|
|
assert.deepEqual(one.map(({ sourceId }) => sourceId), ["sensor.lidar.primary", "rig.front"]);
|
|
assert.deepEqual(many.map(({ sourceId }) => sourceId), [
|
|
"sensor.lidar.primary",
|
|
"sensor.camera.left",
|
|
"sensor.camera.right",
|
|
]);
|
|
assert.equal(new Set(many.map(({ id }) => id)).size, many.length);
|
|
assert.ok(many.every(({ provider }) => provider.pluginId && provider.modelId));
|
|
assert.ok(many.every(({ binding }) => binding.deviceId === "device-k1-001"));
|
|
});
|
|
|
|
test("descriptor ids remain stable while point cloud and selected camera start streaming", () => {
|
|
const declared = xgridsK1ObservationSources(declaredState(), model());
|
|
const streaming = xgridsK1ObservationSources(
|
|
cameraStreamingState("sensor.camera.left"),
|
|
model(),
|
|
);
|
|
|
|
assert.deepEqual(streaming.map(({ id }) => id), declared.map(({ id }) => id));
|
|
assert.equal(declared[0].availability, "available");
|
|
assert.equal(streaming[0].availability, "streaming");
|
|
assert.equal(streaming[0].previewUrl, "rerun+http://127.0.0.1:9877/proxy");
|
|
});
|
|
|
|
test("only the authoritative selected camera receives browser delivery", () => {
|
|
const sources = xgridsK1ObservationSources(
|
|
cameraStreamingState("sensor.camera.left"),
|
|
model(),
|
|
);
|
|
const cameras = sources.filter(({ modality }) => modality === "video");
|
|
const [left, right] = cameras;
|
|
|
|
assert.equal(cameras.length, 2);
|
|
assert.equal(left.activation.selected, true);
|
|
assert.equal(left.activation.maxActive, 1);
|
|
assert.equal(left.availability, "streaming");
|
|
assert.equal(left.delivery.kind, "mse-fmp4-websocket");
|
|
assert.equal(left.transport, "websocket");
|
|
assert.equal(right.activation.selected, false);
|
|
assert.equal(right.availability, "available");
|
|
assert.equal(right.delivery, null);
|
|
assert.equal(left.activation.groupId, right.activation.groupId);
|
|
assert.match(left.activation.groupId, /device-session-001/);
|
|
});
|
|
|
|
test("switching left to right keeps ids stable and never exposes both deliveries", () => {
|
|
const left = xgridsK1ObservationSources(
|
|
cameraStreamingState("sensor.camera.left"),
|
|
model(),
|
|
).filter(({ modality }) => modality === "video");
|
|
const right = xgridsK1ObservationSources(
|
|
cameraStreamingState("sensor.camera.right"),
|
|
model(),
|
|
).filter(({ modality }) => modality === "video");
|
|
|
|
assert.deepEqual(right.map(({ id }) => id), left.map(({ id }) => id));
|
|
assert.deepEqual(left.filter(({ delivery }) => delivery).map(({ sourceId }) => sourceId), [
|
|
"sensor.camera.left",
|
|
]);
|
|
assert.deepEqual(right.filter(({ delivery }) => delivery).map(({ sourceId }) => sourceId), [
|
|
"sensor.camera.right",
|
|
]);
|
|
});
|
|
|
|
test("camera descriptors never leak vendor RTSP endpoints or device IP addresses", () => {
|
|
const sources = xgridsK1ObservationSources(
|
|
cameraStreamingState("sensor.camera.left"),
|
|
model(),
|
|
);
|
|
const cameras = sources.filter(({ modality }) => modality === "video");
|
|
|
|
assert.ok(cameras.every(({ previewUrl }) => previewUrl === null));
|
|
assert.deepEqual(collectUrlLikeStrings(sources), [
|
|
"rerun+http://127.0.0.1:9877/proxy",
|
|
]);
|
|
const serialized = JSON.stringify(cameras);
|
|
assert.doesNotMatch(serialized, /rtsp:\/\//i);
|
|
assert.doesNotMatch(serialized, /192\.168\.7\.10/);
|
|
assert.doesNotMatch(serialized, /vendor-(?:preview|viewer)/);
|
|
});
|
|
|
|
test("unattested, duplicate and unsafe camera entries fail closed", () => {
|
|
const duplicate = cameraRow("sensor.camera.left", "Duplicate");
|
|
const state = cameraStreamingState("sensor.camera.left");
|
|
state.compatibility.profile_id = null;
|
|
state.device_session.compatibility_profile_id = null;
|
|
state.sensor_catalog.streams.push(duplicate);
|
|
state.camera_preview.delivery = {
|
|
...state.camera_preview.delivery,
|
|
url: "ws://192.168.7.10:9000/leak",
|
|
};
|
|
|
|
const cameras = xgridsK1ObservationSources(state, model()).filter(
|
|
({ modality }) => modality === "video",
|
|
);
|
|
assert.deepEqual(cameras.map(({ sourceId }) => sourceId), ["sensor.camera.right"]);
|
|
assert.equal(cameras[0].availability, "unverified");
|
|
assert.equal(cameras[0].activation.controllable, false);
|
|
assert.equal(cameras[0].delivery, null);
|
|
});
|
|
|
|
test("camera delivery rejects literal and encoded endpoint or credential leaks", () => {
|
|
const unsafeUrls = [
|
|
"/api/preview?upstream=rtsp://camera.local/live",
|
|
"/api/preview?upstream=rtsp%3A%2F%2Fcamera.local%2Flive",
|
|
"/api/preview?upstream=rtsp%253A%252F%252Fcamera.local%252Flive",
|
|
"/api/preview?endpoint=192.168.68.52:8554",
|
|
"/api/preview?endpoint=192%2E168%2E68%2E52",
|
|
"/api/preview?password=not-for-the-browser",
|
|
"/api/preview?%70%61%73%73%77%6f%72%64=not-for-the-browser",
|
|
"/api/preview/camera:secret@device",
|
|
"/%2f%2fevil.example/preview",
|
|
];
|
|
|
|
for (const url of unsafeUrls) {
|
|
const state = cameraStreamingState("sensor.camera.left");
|
|
state.camera_preview.delivery = { ...state.camera_preview.delivery, url };
|
|
state.sensor_catalog.streams = state.sensor_catalog.streams.map((stream) =>
|
|
stream.source_id === "sensor.camera.left"
|
|
? { ...stream, delivery: { ...stream.delivery, url } }
|
|
: stream,
|
|
);
|
|
const left = xgridsK1ObservationSources(state, model()).find(
|
|
({ sourceId }) => sourceId === "sensor.camera.left",
|
|
);
|
|
assert.ok(left, `left camera descriptor missing for ${url}`);
|
|
assert.equal(left.delivery, null, `unsafe delivery escaped for ${url}`);
|
|
}
|
|
});
|
|
|
|
test("manual camera reconnect restores an exhausted lease retry budget", () => {
|
|
let budget = resetCameraLeaseRetryBudget("delivery-7");
|
|
for (const expectedDelay of [400, 1_000, 2_000]) {
|
|
const retry = consumeCameraLeaseRetry(budget, "delivery-7");
|
|
assert.equal(retry.delay, expectedDelay);
|
|
budget = retry.budget;
|
|
}
|
|
assert.equal(consumeCameraLeaseRetry(budget, "delivery-7").delay, null);
|
|
|
|
budget = resetCameraLeaseRetryBudget("delivery-7");
|
|
const retryAfterManualReset = consumeCameraLeaseRetry(budget, "delivery-7");
|
|
assert.equal(retryAfterManualReset.delay, 400);
|
|
assert.equal(retryAfterManualReset.budget.count, 1);
|
|
});
|
|
|
|
test("layout policy evicts only exclusive camera peers", () => {
|
|
const sources = xgridsK1ObservationSources(
|
|
cameraStreamingState("sensor.camera.left"),
|
|
model(),
|
|
);
|
|
const left = sources.find(({ sourceId }) => sourceId === "sensor.camera.left");
|
|
const right = sources.find(({ sourceId }) => sourceId === "sensor.camera.right");
|
|
const pointCloud = sources.find(({ modality }) => modality === "point-cloud");
|
|
assert.ok(left && right && pointCloud);
|
|
|
|
const change = openObservationSource(
|
|
[pointCloud.id, left.id],
|
|
right.id,
|
|
sources,
|
|
);
|
|
assert.deepEqual(change.visibleIds, [pointCloud.id, right.id]);
|
|
assert.deepEqual(change.removedIds, [left.id]);
|
|
});
|
|
|
|
test("selected camera without delivery is explicitly restartable", () => {
|
|
const state = cameraStreamingState("sensor.camera.left");
|
|
state.camera_preview = {
|
|
...state.camera_preview,
|
|
phase: "error",
|
|
delivery: null,
|
|
};
|
|
state.sensor_catalog.streams = state.sensor_catalog.streams.map((stream) =>
|
|
stream.source_id === "sensor.camera.left"
|
|
? { ...stream, availability: "error", delivery: null }
|
|
: stream,
|
|
);
|
|
const sources = xgridsK1ObservationSources(state, model());
|
|
const left = sources.find(({ sourceId }) => sourceId === "sensor.camera.left");
|
|
const right = sources.find(({ sourceId }) => sourceId === "sensor.camera.right");
|
|
assert.ok(left && right);
|
|
|
|
assert.equal(left.activation.selected, true);
|
|
assert.equal(left.delivery, null);
|
|
assert.equal(left.availability, "error");
|
|
assert.equal(shouldRestartObservationSource(left), true);
|
|
assert.equal(shouldRestartObservationSource(right), false);
|
|
});
|