359 lines
11 KiB
JavaScript
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(
|
|
"/src/device-plugins/xgrids-k1/manifest.ts",
|
|
));
|
|
lifecycle = await server.ssrLoadModule(
|
|
"/src/device-plugins/xgrids-k1/lifecycle.ts",
|
|
);
|
|
({ xgridsK1Api } = await server.ssrLoadModule(
|
|
"/src/device-plugins/xgrids-k1/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);
|
|
});
|