feat(map): open bounded telemetry card from entities
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import { buildMapSubjectCardModel, normalizeTelemetryReadings } from "../apps/catalog/src/mapSubjectCard.mjs";
|
||||
|
||||
const fact = {
|
||||
sourceId: "867236078012345",
|
||||
semanticType: "map.moving_object",
|
||||
presentationStatus: "current",
|
||||
observedAt: "2026-07-22T08:42:03.000Z",
|
||||
receivedAt: "2026-07-22T08:42:05.000Z",
|
||||
geometry: { type: "Point", coordinates: [37.6176, 55.7558] },
|
||||
attributes: {
|
||||
display_name: "555590 B2 867236078012345",
|
||||
signal_state: "active",
|
||||
movement_state: "moving",
|
||||
speed_kph: 17.4,
|
||||
course_degrees: 211,
|
||||
elevation_meters: 142,
|
||||
satellite_count: 12,
|
||||
hdop: 0.7,
|
||||
horizontal_accuracy_meters: 3.5,
|
||||
mileage_km: 4832.25,
|
||||
engine_hours: 912.5,
|
||||
object_kind: "trike",
|
||||
position_source: "gelios",
|
||||
sensor_readings: [
|
||||
{ id: "sensor.temperature", label: "Температура", value: 21.5, unit: "°C", observedAt: "2026-07-22T08:42:02.000Z" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
test("subject card exposes the full provider-neutral telemetry projection", () => {
|
||||
const card = buildMapSubjectCardModel(fact, {
|
||||
bindingId: "trike-current-positions",
|
||||
dataProductId: "fleet.positions.current.v5",
|
||||
});
|
||||
|
||||
assert.ok(card);
|
||||
assert.equal(card.title, "555590 B2 867236078012345");
|
||||
assert.equal(card.sourceId, fact.sourceId);
|
||||
const values = new Map(card.sections.flatMap((section) => section.rows.map((row) => [row.key, row.value])));
|
||||
assert.equal(values.get("source_id"), fact.sourceId);
|
||||
assert.equal(values.get("data_product_id"), "fleet.positions.current.v5");
|
||||
assert.equal(values.get("binding_id"), "trike-current-positions");
|
||||
assert.equal(values.get("latitude"), "55,756");
|
||||
assert.equal(values.get("longitude"), "37,618");
|
||||
assert.equal(values.get("signal_state"), "На связи");
|
||||
assert.equal(values.get("movement_state"), "В движении");
|
||||
assert.equal(values.get("speed_kph"), "17,4 км/ч");
|
||||
assert.equal(values.get("mileage_km"), "4 832,25 км");
|
||||
assert.equal(values.get("engine_hours"), "912,5 ч");
|
||||
assert.equal(card.readings[0]?.value, "21,5 °C");
|
||||
});
|
||||
|
||||
test("sensor readings remain bounded and reject duplicate, complex and restricted values", () => {
|
||||
const readings = normalizeTelemetryReadings([
|
||||
{ id: "sensor.voltage", label: "Напряжение", value: 48.2, unit: "V" },
|
||||
{ id: "sensor.voltage", label: "Повтор", value: 49 },
|
||||
{ id: "sensor.imei", label: "IMEI", value: "forbidden" },
|
||||
{ id: "sensor.payload", label: "Payload", value: { nested: true } },
|
||||
{ id: "Sensor Bad", label: "Bad id", value: 1 },
|
||||
]);
|
||||
|
||||
assert.deepEqual(readings, [{ id: "sensor.voltage", label: "Напряжение", value: "48,2 V", observedAt: undefined }]);
|
||||
});
|
||||
|
||||
test("card model never renders provider secrets or direct identifiers from attributes", () => {
|
||||
const card = buildMapSubjectCardModel({
|
||||
...fact,
|
||||
attributes: {
|
||||
...fact.attributes,
|
||||
api_key: "forbidden",
|
||||
imei: "forbidden",
|
||||
phone: "+70000000000",
|
||||
raw_params: "forbidden",
|
||||
},
|
||||
});
|
||||
const keys = card.sections.flatMap((section) => section.rows.map((row) => row.key));
|
||||
assert.ok(!keys.includes("api_key"));
|
||||
assert.ok(!keys.includes("imei"));
|
||||
assert.ok(!keys.includes("phone"));
|
||||
assert.ok(!keys.includes("raw_params"));
|
||||
});
|
||||
|
||||
test("Cesium pin and label share one entity selection that opens the subject card", async () => {
|
||||
const [renderer, preview] = await Promise.all([
|
||||
readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"),
|
||||
]);
|
||||
|
||||
assert.match(renderer, /viewer\?\.scene\.pick/);
|
||||
assert.match(renderer, /pickedId instanceof Entity/);
|
||||
assert.match(renderer, /onSelectRef\.current\?\.\(pickedId\.id\)/);
|
||||
assert.match(preview, /const handleSelect = useCallback/);
|
||||
assert.match(preview, /setSubjectCardOpen\(true\)/);
|
||||
assert.match(preview, /title=\{selectedSubjectCard\.title\}/);
|
||||
assert.match(preview, /Телеметрия:/);
|
||||
});
|
||||
@@ -110,6 +110,12 @@ requireValue(movingObjectV4Policy?.statusContract?.attribute === "signal_state",
|
||||
requireValue(JSON.stringify(movingObjectV4Policy?.statusContract?.allowedValues) === JSON.stringify(["active", "inactive"]), "fleet.positions.current.v4 signal states must remain closed active/inactive");
|
||||
requireValue(movingObjectV4Policy?.statusContract?.freshness === "none" && movingObjectV4Policy?.staleAfterMs === null, "fleet.positions.current.v4 must not invent freshness states");
|
||||
|
||||
const movingObjectV5Policy = consumerPolicies.find((policy) => policy.dataProductId === "fleet.positions.current.v5" && policy.productVersion === "5.0.0");
|
||||
requireValue(movingObjectV5Policy?.id === "map-moving-object-current-v5", "fleet.positions.current.v5 consumer policy is missing");
|
||||
requireValue(movingObjectV5Policy?.statusContract?.attribute === "signal_state", "fleet.positions.current.v5 status source must be signal_state");
|
||||
requireValue(JSON.stringify(movingObjectV5Policy?.statusContract?.allowedValues) === JSON.stringify(["active", "inactive"]), "fleet.positions.current.v5 signal states must remain closed active/inactive");
|
||||
requireValue(movingObjectV5Policy?.statusContract?.freshness === "none" && movingObjectV5Policy?.staleAfterMs === null, "fleet.positions.current.v5 must not invent freshness states");
|
||||
|
||||
const zoneV1Policy = consumerPolicies.find((policy) => policy.dataProductId === "map.zones.current.v1" && policy.productVersion === "1.0.0");
|
||||
requireValue(JSON.stringify(zoneV1Policy) === JSON.stringify({
|
||||
id: "map-zone-current-v1",
|
||||
|
||||
Reference in New Issue
Block a user