226 lines
9.2 KiB
JavaScript
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]);
|
|
});
|