import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; import { buildMapSubjectCardModel, DEFAULT_MAP_SUBJECT_DETAIL_PROFILE, normalizeTelemetryReadings } from "../apps/catalog/src/mapSubjectCard.mjs"; const fact = { sourceId: "gelios-unit-555590", 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 profile = structuredClone(DEFAULT_MAP_SUBJECT_DETAIL_PROFILE); profile.tabs.find((tab) => tab.id === "telemetry").sections[0].fields[0].allowedReadingIds = ["sensor.temperature"]; const card = buildMapSubjectCardModel(fact, { bindingId: "trike-current-positions", dataProductId: "fleet.positions.current.v5", profile, }); assert.ok(card); assert.equal(card.title, "555590 B2 867236078012345"); assert.equal(card.sourceId, fact.sourceId); assert.equal(card.defaultTabId, "overview"); assert.deepEqual(card.tabs.map((tab) => tab.id), ["overview", "position", "telemetry", "counters", "equipment", "settings", "maintenance", "diagnostics"]); const values = new Map(card.tabs.flatMap((tab) => tab.sections.flatMap((section) => section.rows.map((row) => [row.key, row.value])))); assert.equal(values.get("diagnostics.source-id"), fact.sourceId); assert.equal(values.get("diagnostics.product"), "fleet.positions.current.v5"); assert.equal(values.get("position.latitude"), "55,75580"); assert.equal(values.get("position.longitude"), "37,61760"); assert.equal(values.get("overview.signal"), "На связи"); assert.equal(values.get("overview.movement"), "В движении"); assert.equal(values.get("overview.speed"), "17,4 км/ч"); assert.equal(values.get("counters.mileage"), "4 832,25 км"); assert.equal(values.get("counters.engine-hours"), "912,5 ч"); const readings = card.tabs.find((tab) => tab.id === "telemetry").sections[0].readings; assert.equal(readings[0]?.value, "21,5 °C"); assert.equal(card.tabs.find((tab) => tab.id === "equipment").empty, true); }); 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 }, { id: "sensor.account", label: "Аккаунт", value: "+79991234567" }, ], ["sensor.voltage", "sensor.imei", "sensor.payload", "sensor.account"]); 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.tabs.flatMap((tab) => tab.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("unknown fields are fail-closed instead of becoming an automatic Other section", () => { const card = buildMapSubjectCardModel({ ...fact, attributes: { ...fact.attributes, unclassified_payload: { arbitrary: true }, surprise_value: "must not render" }, }); const keys = card.tabs.flatMap((tab) => tab.sections.flatMap((section) => section.rows.map((row) => row.key))); assert.ok(!keys.includes("unclassified_payload")); assert.ok(!keys.includes("surprise_value")); assert.equal(card.tabs.some((tab) => tab.id === "other"), false); }); test("subject details compose declared aspect bindings by stable source id", () => { const profile = { id: "map.subject-detail.composed", version: "1.0.0", title: "Composed", semanticTypes: ["map.moving_object"], defaultTabId: "equipment", tabs: [{ id: "equipment", label: "Оборудование", emptyMessage: "Нет данных", sections: [{ id: "identity", label: "Профиль", fields: [{ id: "equipment.kind", aspectId: "technical", source: "attribute", field: "equipment_kind", label: "Тип", format: "text" }], }], }], }; const card = buildMapSubjectCardModel(fact, { profile, aspects: { technical: { bindingId: "trike-technical-profile", dataProductId: "fleet.technical-profile.current.v1", fact: { sourceId: fact.sourceId, semanticType: "asset.technical_profile", attributes: { equipment_kind: "cargo_trike" } }, }, }, }); assert.equal(card.tabs[0].sections[0].rows[0].value, "cargo_trike"); }); 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, /Карточка объекта:/); assert.match(preview, /SegmentedControl/); });