Files
NODEDC_MISSION_CORE/apps/control-station/test/systemTelemetry.test.mjs
T

226 lines
9.2 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { test } from "node:test";
import ts from "typescript";
const sourceRoot = new URL("../src/", import.meta.url);
async function read(relativePath) {
return readFile(new URL(relativePath, sourceRoot), "utf8");
}
test("Compute-contour telemetry remains a bounded system feature slice", async () => {
const [
productModel,
workspaceHub,
core,
computeWorkspace,
pipelineStages,
networkWorkspace,
telemetryPolling,
telemetryContext,
panelActions,
contourSettingsHook,
contourSettings,
contourContract,
pollIntervalContract,
pollingScheduler,
telemetryStyles,
styles,
] = await Promise.all([
read("productModel.ts"),
read("workspaces/Workspaces.tsx"),
read("core/system/workerTelemetry.ts"),
read("workspaces/system/ComputeModulesWorkspace.tsx"),
read("components/system/WorkerPipelineStages.tsx"),
read("workspaces/system/NetworkWorkspace.tsx"),
read("core/system/useWorkerTelemetry.ts"),
read("core/system/ComputeContourContext.tsx"),
read("components/useApplicationPanelActions.ts"),
read("components/system/useComputeContourSettings.tsx"),
read("components/system/ComputeContourSettingsWindow.tsx"),
read("core/system/computeContours.ts"),
read("core/system/telemetryPollInterval.ts"),
read("core/system/sequentialPolling.ts"),
read("styles/system-telemetry.css"),
read("styles.css"),
]);
assert.match(productModel, /kind: "compute-modules"/);
assert.match(productModel, /kind: "network-monitor"/);
assert.match(workspaceHub, /<ComputeModulesWorkspace \/>/);
assert.match(workspaceHub, /<NetworkWorkspace \/>/);
assert.doesNotMatch(workspaceHub, /worker-telemetry|worker-profile|DESKTOP-OPJ8J04/);
assert.match(core, /\/api\/v1\/system\/contours\/.*\/telemetry/);
assert.match(core, /\/api\/v1\/system\/worker-profile/);
assert.match(core, /share_percent/);
assert.doesNotMatch(core, /@nodedc\/ui-react/);
assert.match(computeWorkspace, /useWorkerTelemetry/);
assert.match(computeWorkspace, /telemetry_poll_interval_seconds/);
assert.doesNotMatch(computeWorkspace, />\s*Обновить\s*</);
assert.match(computeWorkspace, /resource=\{formatCpuCapacity/);
assert.match(computeWorkspace, /resource=\{formatResourcePair/);
assert.doesNotMatch(computeWorkspace, /loading\s*\?\s*"Читаем"/);
assert.match(computeWorkspace, /WorkerPipelineStages/);
assert.match(pipelineStages, /измеренного времени/);
assert.doesNotMatch(pipelineStages, /CPU|GPU|hardware/);
assert.match(
telemetryPolling,
/DEFAULT_WORKER_TELEMETRY_POLL_MILLISECONDS\s*=\s*3_000/,
);
assert.match(
telemetryStyles,
/\.worker-stage-list\s*\{[^}]*grid-template-columns:\s*repeat\(\s*auto-fit,\s*minmax\(min\(100%,\s*24rem\),\s*1fr\)/s,
);
assert.doesNotMatch(
telemetryStyles,
/\bborder:\s*1px\s+solid/,
"System product blocks must not reintroduce decorative perimeter outlines",
);
assert.match(networkWorkspace, /127\.0\.0\.1:8000/);
assert.match(networkWorkspace, /telemetry_poll_interval_seconds/);
assert.doesNotMatch(networkWorkspace, />\s*Обновить\s*</);
assert.doesNotMatch(networkWorkspace, /loading\s*\?\s*"Читаем"/);
assert.doesNotMatch(networkWorkspace, /8765/);
assert.match(telemetryContext, /telemetryRefreshGeneration/);
assert.match(telemetryContext, /refreshTelemetry/);
assert.match(telemetryPolling, /externalRefreshGeneration/);
assert.match(panelActions, /actions\.push\(\.\.\.systemUtilityActions\)/);
assert.match(contourSettingsHook, /icon: "refresh"/);
assert.match(contourSettingsHook, /icon: "settings"/);
assert.doesNotMatch(contourSettingsHook, /contour_id\s*!==\s*"worker-006"/);
assert.doesNotMatch(
computeWorkspace,
/contour_id\s*===\s*"worker-006"[\s\S]*telemetry_mode/,
);
assert.doesNotMatch(
networkWorkspace,
/contour_id\s*===\s*"worker-006"[\s\S]*telemetry_mode/,
);
assert.doesNotMatch(contourSettings, /<dt>Worker 006<\/dt>/);
assert.match(contourSettings, /contour\?\.display_name/);
assert.match(telemetryPolling, /Вычислительный контур не ответил\./);
assert.match(contourSettings, /FieldFrame label="Операционная система"/);
assert.match(contourSettings, /label="Обновление интерфейса"/);
assert.match(contourSettings, /label="Интервал MQTT агента"/);
assert.match(contourSettings, /label="Публикация broker"/);
assert.match(contourSettings, /MQTT · Mac ↔ Worker/);
assert.match(contourSettings, /applyComputeContourNetwork/);
assert.match(contourSettings, /Применить на Worker/);
assert.match(contourSettings, /value=\{telemetryPollIntervalDraft\}/);
assert.match(contourSettings, /onBlur=\{commitTelemetryPollInterval\}/);
assert.match(contourSettings, /event\.key === "Escape"/);
assert.doesNotMatch(
contourSettings,
/telemetry_poll_interval_seconds:\s*Number\(event\.currentTarget\.value\)/,
);
assert.match(contourContract, /telemetry_poll_interval_seconds: number/);
assert.match(contourContract, /mqtt_publish_interval_seconds: number/);
assert.match(contourContract, /mqtt_bind_address: string/);
assert.match(
contourContract,
/missioncore\.compute-contour-network-status\/v1/,
);
assert.match(pollIntervalContract, /MIN_TELEMETRY_POLL_INTERVAL_SECONDS\s*=\s*1/);
assert.match(telemetryPolling, /normalizeWorkerTelemetryPollMilliseconds/);
assert.match(telemetryPolling, /startSequentialPolling/);
assert.match(telemetryPolling, /catch \(reason: unknown\)[\s\S]*?setTelemetry\(null\)/);
assert.match(computeWorkspace, /sm_clock_mhz/);
assert.match(computeWorkspace, /memory_clock_mhz/);
assert.match(computeWorkspace, /Ожидает восстановления данных/);
assert.doesNotMatch(computeWorkspace, /Очередь свободна; модели остаются загруженными/);
assert.doesNotMatch(telemetryPolling, /if\s*\(\s*!enabled\s*\|\|\s*loading\s*\)/);
assert.match(pollingScheduler, /A transient failure must not stop polling/);
assert.match(styles, /system-telemetry\.css/);
});
test("MQTT polling interval keeps an unrestricted draft until explicit commit", async () => {
const source = await read("core/system/telemetryPollInterval.ts");
const javascript = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.ES2022,
target: ts.ScriptTarget.ES2022,
},
}).outputText;
const contract = await import(
`data:text/javascript;base64,${Buffer.from(javascript).toString("base64")}`
);
assert.equal(contract.parseTelemetryPollIntervalDraft(""), null);
assert.equal(contract.parseTelemetryPollIntervalDraft("0"), null);
assert.equal(contract.parseTelemetryPollIntervalDraft("0,1"), null);
assert.equal(contract.parseTelemetryPollIntervalDraft("0.1"), null);
assert.equal(contract.parseTelemetryPollIntervalDraft("1"), 1);
assert.equal(contract.parseTelemetryPollIntervalDraft("60"), 60);
assert.equal(contract.parseTelemetryPollIntervalDraft("61"), null);
assert.deepEqual(contract.resolveTelemetryPollIntervalDraft("", 3), {
accepted: false,
draft: "3",
seconds: 3,
});
assert.deepEqual(contract.resolveTelemetryPollIntervalDraft("01", 3), {
accepted: true,
draft: "1",
seconds: 1,
});
assert.equal(contract.normalizeWorkerTelemetryPollMilliseconds(100), 1_000);
assert.equal(contract.normalizeWorkerTelemetryPollMilliseconds(3_000), 3_000);
});
test("Worker telemetry polling repeats after fast responses and transient errors", async () => {
const source = await read("core/system/sequentialPolling.ts");
const javascript = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.ES2022,
target: ts.ScriptTarget.ES2022,
},
}).outputText;
const polling = await import(
`data:text/javascript;base64,${Buffer.from(javascript).toString("base64")}`
);
let nextHandle = 1;
const timers = new Map();
const cleared = [];
const scheduler = {
setTimeout(callback, delayMilliseconds) {
const handle = nextHandle++;
timers.set(handle, { callback, delayMilliseconds });
return handle;
},
clearTimeout(handle) {
cleared.push(handle);
timers.delete(handle);
},
};
let attempts = 0;
const stop = polling.startSequentialPolling(async () => {
attempts += 1;
if (attempts === 2) throw new Error("transient");
}, 3_000, scheduler);
await new Promise((resolve) => setImmediate(resolve));
assert.equal(attempts, 1);
assert.equal(timers.size, 1);
assert.equal([...timers.values()][0].delayMilliseconds, 3_000);
let [handle, scheduled] = [...timers.entries()][0];
timers.delete(handle);
scheduled.callback();
await new Promise((resolve) => setImmediate(resolve));
assert.equal(attempts, 2);
assert.equal(timers.size, 1, "a transient error must schedule the next poll");
[handle, scheduled] = [...timers.entries()][0];
timers.delete(handle);
scheduled.callback();
await new Promise((resolve) => setImmediate(resolve));
assert.equal(attempts, 3);
assert.equal(timers.size, 1);
stop();
assert.equal(timers.size, 0);
assert.deepEqual(cleared, [nextHandle - 1]);
});