NODEDC_MISSION_CORE/apps/control-station/test/devicePluginContracts.test.mjs

359 lines
11 KiB
JavaScript

import assert from "node:assert/strict";
import { after, before, test } from "node:test";
import { createServer } from "vite";
let server;
let parseDevicePluginManifest;
let createDevicePluginRegistry;
let xgridsK1Manifest;
let xgridsK1Actions;
let xgridsK1Api;
let lifecycle;
before(async () => {
server = await createServer({
appType: "custom",
logLevel: "silent",
server: { middlewareMode: true },
});
({ parseDevicePluginManifest } = await server.ssrLoadModule(
"/src/core/device-plugins/manifestParser.ts",
));
({ createDevicePluginRegistry } = await server.ssrLoadModule(
"/src/core/device-plugins/registry.ts",
));
({ xgridsK1Manifest, xgridsK1Actions } = await server.ssrLoadModule(
"@xgrids-k1/frontend/manifest.ts",
));
lifecycle = await server.ssrLoadModule(
"@xgrids-k1/frontend/lifecycle.ts",
);
({ xgridsK1Api } = await server.ssrLoadModule(
"@xgrids-k1/frontend/api.ts",
));
});
after(async () => {
await server?.close();
});
function manifestDocument({
apiVersion = "missioncore.nodedc/v1alpha1",
pluginId = "test.device.plugin",
modelId = "test.device.model",
} = {}) {
const v1alpha2 = apiVersion === "missioncore.nodedc/v1alpha2";
return {
apiVersion,
kind: "DevicePlugin",
metadata: {
id: pluginId,
version: "1.0.0",
displayName: "Test device",
},
spec: {
hostApiRange: v1alpha2 ? "v1alpha2" : "v1alpha1",
runtime: {
backendEntrypoint: "test.plugin:build",
isolation: "transitional-in-process",
},
...(v1alpha2
? {
compatibilityProfiles: [
{
profileId: `${modelId}.fw-1.v1`,
path: "profiles/fw-1/profile.v1.json",
modelId,
},
],
}
: {}),
permissions: ["device.read"],
actions: [{ id: "state.read", mutating: false, secretFields: [] }],
models: [
{
id: modelId,
vendor: "Test",
displayName: "Test model",
category: "Sensor",
description: "Fixture model",
verified: true,
capabilities: [{ id: "device.read", label: "Read" }],
ui: { slot: "device.connection", componentKey: "test.connection" },
},
],
},
};
}
function uiPlugin(manifest) {
return {
manifest,
RuntimeProvider: ({ children }) => children,
connectionViews: { "test.connection": () => null },
};
}
test("parser accepts reviewed v1alpha1 and v1alpha2 shapes", () => {
const legacy = parseDevicePluginManifest(manifestDocument());
const current = parseDevicePluginManifest(
manifestDocument({ apiVersion: "missioncore.nodedc/v1alpha2" }),
);
assert.equal(legacy.apiVersion, "missioncore.nodedc/v1alpha1");
assert.equal(legacy.spec.hostApiRange, "v1alpha1");
assert.equal(current.apiVersion, "missioncore.nodedc/v1alpha2");
assert.equal(current.spec.hostApiRange, "v1alpha2");
assert.deepEqual(current.spec.compatibilityProfiles, [
{
profileId: "test.device.model.fw-1.v1",
path: "profiles/fw-1/profile.v1.json",
modelId: "test.device.model",
},
]);
});
test("v1alpha1 keeps the original nonblank identifier compatibility", () => {
const legacyDocument = manifestDocument({
pluginId: "legacy plugin id",
modelId: "legacy model id",
});
legacyDocument.spec.permissions = ["legacy permission"];
legacyDocument.spec.actions[0].secretFields = ["legacy secret field"];
legacyDocument.spec.models[0].capabilities = [
{ id: "legacy capability", label: "Legacy" },
];
const legacy = parseDevicePluginManifest(legacyDocument);
assert.equal(legacy.metadata.id, "legacy plugin id");
assert.equal(legacy.spec.models[0].id, "legacy model id");
assert.deepEqual(legacy.spec.permissions, ["legacy permission"]);
});
test("v1alpha2 applies strict identifiers without redefining v1alpha1", () => {
const current = manifestDocument({
apiVersion: "missioncore.nodedc/v1alpha2",
pluginId: "current plugin id",
});
assert.throws(
() => parseDevicePluginManifest(current),
/не является идентификатором/,
);
});
test("v1alpha2 parser and registry accept multiple independently profiled models", () => {
const document = manifestDocument({
apiVersion: "missioncore.nodedc/v1alpha2",
pluginId: "test.family",
modelId: "test.family.model-a",
});
document.spec.models.push({
...document.spec.models[0],
id: "test.family.model-b",
displayName: "Test model B",
});
document.spec.compatibilityProfiles.push({
profileId: "test.family.model-b.fw-1.v1",
path: "profiles/fw-1/model-b.v1.json",
modelId: "test.family.model-b",
});
const manifest = parseDevicePluginManifest(document);
const plugin = {
...uiPlugin(manifest),
connectionViews: { "test.connection": () => null },
};
const registry = createDevicePluginRegistry([plugin]);
assert.deepEqual(registry.models.map(({ model }) => model.id), [
"test.family.model-a",
"test.family.model-b",
]);
});
test("installed XGRIDS frontend manifest exposes the semantic v1alpha2 actions", () => {
assert.equal(xgridsK1Manifest.apiVersion, "missioncore.nodedc/v1alpha2");
assert.deepEqual(xgridsK1Manifest.spec.compatibilityProfiles, [
{
profileId: "xgrids.lixelkity-k1.fw-3.0.2.direct-lan.v1",
path: "profiles/fw-3.0.2/direct-lan.v1.json",
modelId: "xgrids.lixelkity-k1",
},
]);
assert.equal(xgridsK1Actions.acquisitionPrepare, "acquisition.prepare");
assert.equal(xgridsK1Actions.acquisitionStart, "acquisition.start");
assert.equal(xgridsK1Actions.acquisitionStop, "acquisition.stop");
});
test("v1alpha2 compatibility profile is exact-key and rejects duplicated profile status", () => {
const document = manifestDocument({ apiVersion: "missioncore.nodedc/v1alpha2" });
document.spec.compatibilityProfiles[0].status = "verified";
assert.throws(
() => parseDevicePluginManifest(document),
/неизвестные поля status/,
);
});
test("v1alpha2 compatibility profile fails closed on unsafe paths", () => {
const document = manifestDocument({ apiVersion: "missioncore.nodedc/v1alpha2" });
document.spec.compatibilityProfiles[0].path = "../private/profile.json";
assert.throws(
() => parseDevicePluginManifest(document),
/безопасным относительным JSON-путём/,
);
});
test("v1alpha2 compatibility profile cannot reference an unknown model", () => {
const document = manifestDocument({ apiVersion: "missioncore.nodedc/v1alpha2" });
document.spec.compatibilityProfiles[0].modelId = "missing.model";
assert.throws(
() => parseDevicePluginManifest(document),
/ссылается на неизвестную модель missing\.model/,
);
});
test("registry accepts v1alpha1 and v1alpha2 plugins together", () => {
const legacy = parseDevicePluginManifest(
manifestDocument({ pluginId: "test.legacy", modelId: "test.legacy.model" }),
);
const current = parseDevicePluginManifest(
manifestDocument({
apiVersion: "missioncore.nodedc/v1alpha2",
pluginId: "test.current",
modelId: "test.current.model",
}),
);
const registry = createDevicePluginRegistry([uiPlugin(legacy), uiPlugin(current)]);
assert.equal(registry.plugins.length, 2);
assert.equal(registry.models.length, 2);
assert.equal(registry.resolveModel("test.current.model")?.plugin.manifest.metadata.id, "test.current");
});
test("registry independently rejects an uncovered v1alpha2 model", () => {
const current = parseDevicePluginManifest(
manifestDocument({ apiVersion: "missioncore.nodedc/v1alpha2" }),
);
current.spec.compatibilityProfiles[0].modelId = "missing.model";
assert.throws(
() => createDevicePluginRegistry([uiPlugin(current)]),
/ссылается на неизвестную модель missing\.model/,
);
});
test("live source is confirmed only by an acquiring acquisition", () => {
const waiting = {
source_mode: "live",
phase: "live",
acquisition: { state: "awaiting_external_start" },
};
const acquiring = {
...waiting,
acquisition: { state: "acquiring" },
};
assert.equal(lifecycle.isConfirmedLiveState(waiting), false);
assert.equal(lifecycle.confirmedRuntimeSourceMode(waiting), "idle");
assert.equal(lifecycle.sourceStatusLabel(waiting), "Ожидание реальных данных");
assert.equal(lifecycle.isConfirmedLiveState(acquiring), true);
assert.equal(lifecycle.confirmedRuntimeSourceMode(acquiring), "live");
});
test("prepared acquisition resumes without another prepare and remains recoverable", () => {
const prepared = {
source_mode: "idle",
acquisition: {
acquisition_id: "acq-1",
state: "prepared",
},
};
assert.equal(lifecycle.liveStartPlan(prepared), "resume-prepared");
assert.equal(lifecycle.recoverableAcquisition(prepared)?.acquisition_id, "acq-1");
});
test("replay ignores a stale failed live acquisition", () => {
const replay = {
source_mode: "replay",
phase: "replay",
rerun_grpc_url: "rerun+http://127.0.0.1:9876/proxy",
acquisition: {
acquisition_id: "old-live-acquisition",
state: "failed",
},
};
assert.equal(lifecycle.normalizeRuntimePhase(replay), "replaying");
assert.equal(lifecycle.effectiveAcquisition(replay), null);
assert.equal(
lifecycle.spatialSourceId(replay, replay.rerun_grpc_url),
`replay:${replay.rerun_grpc_url}`,
);
});
test("provisioning intent keeps one idempotency key and exposes unsafe outcomes", () => {
let created = 0;
const createUuid = () => {
created += 1;
return "11111111-1111-4111-8111-111111111111";
};
const first = lifecycle.provisioningIntentKey(null, createUuid);
const repeated = lifecycle.provisioningIntentKey(first, createUuid);
const failedOperation = {
status: "failed",
error: { safe_to_retry: false },
};
assert.equal(first, "network-provision:11111111-1111-4111-8111-111111111111");
assert.equal(repeated, first);
assert.equal(created, 1);
assert.equal(lifecycle.operationNeedsReconciliation(failedOperation), true);
});
test("device mutations send explicit nested compatibility attestation", async () => {
const originalFetch = globalThis.fetch;
const calls = [];
const syntheticCredential = "x".repeat(32);
globalThis.fetch = async (path, init) => {
calls.push({ path, init });
return new Response(JSON.stringify({ state: { source_mode: "idle" } }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
const attestation = {
firmware_version: "3.0.2",
topology: "direct-lan",
operator_confirmed: true,
};
try {
await xgridsK1Api.connect({
device_id: "ble-device",
ssid: "lab-network",
password: syntheticCredential,
compatibility_attestation: attestation,
idempotency_key: "network-provision:test",
});
await xgridsK1Api.prepareAcquisition({
compatibility_attestation: attestation,
});
} finally {
globalThis.fetch = originalFetch;
}
assert.equal(calls.length, 2);
const provisioning = JSON.parse(calls[0].init.body);
const prepare = JSON.parse(calls[1].init.body);
assert.deepEqual(provisioning.input.compatibility_attestation, attestation);
assert.equal(provisioning.input.idempotency_key, "network-provision:test");
assert.deepEqual(prepare.input.compatibility_attestation, attestation);
});