754 lines
26 KiB
JavaScript
754 lines
26 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { readFile } from "node:fs/promises";
|
|
import { after, before, test } from "node:test";
|
|
|
|
import { createServer } from "vite";
|
|
|
|
let server;
|
|
let parseDevicePluginManifest;
|
|
let createDevicePluginRegistry;
|
|
let xgridsK1Manifest;
|
|
let xgridsK1Actions;
|
|
let xgridsK1Api;
|
|
let ApiError;
|
|
let localizeRuntimeMessage;
|
|
let lifecycle;
|
|
let projectName;
|
|
let automaticSourceStart;
|
|
let presentation;
|
|
let operatorIntentGeneration;
|
|
let configuration;
|
|
let compatibility;
|
|
let networkProvisionFailureMessage;
|
|
|
|
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",
|
|
);
|
|
projectName = await server.ssrLoadModule(
|
|
"@xgrids-k1/frontend/projectName.ts",
|
|
);
|
|
automaticSourceStart = await server.ssrLoadModule(
|
|
"@xgrids-k1/frontend/automaticSourceStart.ts",
|
|
);
|
|
presentation = await server.ssrLoadModule(
|
|
"@xgrids-k1/frontend/presentation.ts",
|
|
);
|
|
operatorIntentGeneration = await server.ssrLoadModule(
|
|
"@xgrids-k1/frontend/operatorIntentGeneration.ts",
|
|
);
|
|
configuration = await server.ssrLoadModule(
|
|
"@xgrids-k1/frontend/configuration.ts",
|
|
);
|
|
compatibility = await server.ssrLoadModule(
|
|
"@xgrids-k1/frontend/compatibility.ts",
|
|
);
|
|
({ xgridsK1Api, ApiError } = await server.ssrLoadModule(
|
|
"@xgrids-k1/frontend/api.ts",
|
|
));
|
|
({ localizeRuntimeMessage } = await server.ssrLoadModule(
|
|
"@xgrids-k1/frontend/messages.ts",
|
|
));
|
|
({ networkProvisionFailureMessage } = await server.ssrLoadModule(
|
|
"@xgrids-k1/frontend/useXgridsK1Runtime.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.local-network.v2",
|
|
path: "profiles/fw-3.0.2/local-network.v2.json",
|
|
modelId: "xgrids.lixelkity-k1",
|
|
},
|
|
]);
|
|
assert.equal(xgridsK1Actions.acquisitionPrepare, "acquisition.prepare");
|
|
assert.equal(xgridsK1Actions.acquisitionStart, "acquisition.start");
|
|
assert.equal(xgridsK1Actions.acquisitionStop, "acquisition.stop");
|
|
assert.equal(xgridsK1Actions.connectionVerify, "connection.verify");
|
|
});
|
|
|
|
test("one operator action can open at most one K1 control session", () => {
|
|
assert.equal(lifecycle.controlSessionEntryPlan("idle", false, true), "open");
|
|
assert.equal(lifecycle.controlSessionEntryPlan("failed", false, true), "open");
|
|
assert.equal(lifecycle.controlSessionEntryPlan("failed", false, false), "failed");
|
|
assert.equal(lifecycle.controlSessionEntryPlan("failed", true, true), "failed");
|
|
assert.equal(
|
|
lifecycle.controlSessionEntryPlan("idle", true, true),
|
|
"duplicate-open",
|
|
);
|
|
assert.equal(
|
|
lifecycle.controlSessionEntryPlan("connection-ready", true, false),
|
|
"continue",
|
|
);
|
|
});
|
|
|
|
test("K1 operator intent stays invalid after runtime deactivate and reactivate", () => {
|
|
const generation = new operatorIntentGeneration.OperatorIntentGeneration();
|
|
generation.activateRuntime();
|
|
const oldIntent = generation.beginOperatorIntent();
|
|
|
|
assert.ok(oldIntent);
|
|
assert.equal(generation.isOperatorIntentCurrent(oldIntent), true);
|
|
|
|
generation.deactivateRuntime();
|
|
generation.activateRuntime();
|
|
const freshIntent = generation.beginOperatorIntent();
|
|
|
|
assert.ok(freshIntent);
|
|
assert.notEqual(freshIntent.runtimeGeneration, oldIntent.runtimeGeneration);
|
|
assert.equal(generation.isOperatorIntentCurrent(oldIntent), false);
|
|
assert.equal(generation.isOperatorIntentCurrent(freshIntent), true);
|
|
});
|
|
|
|
test("stale K1 intent cannot continue past an await after runtime reactivation", async () => {
|
|
const generation = new operatorIntentGeneration.OperatorIntentGeneration();
|
|
generation.activateRuntime();
|
|
const oldIntent = generation.beginOperatorIntent();
|
|
assert.ok(oldIntent);
|
|
|
|
let resolveOldRead;
|
|
const oldRead = new Promise((resolve) => {
|
|
resolveOldRead = resolve;
|
|
});
|
|
const writes = [];
|
|
const assertOldIntent = () => {
|
|
if (!generation.isOperatorIntentCurrent(oldIntent)) {
|
|
throw new Error("stale operator intent");
|
|
}
|
|
};
|
|
const oldContinuation = operatorIntentGeneration.awaitWhileIntentCurrent(
|
|
assertOldIntent,
|
|
() => oldRead,
|
|
).then(() => writes.push("old-checkpoint"));
|
|
|
|
generation.deactivateRuntime();
|
|
generation.activateRuntime();
|
|
const freshIntent = generation.beginOperatorIntent();
|
|
assert.ok(freshIntent);
|
|
resolveOldRead({ phase: "connection-ready" });
|
|
|
|
await assert.rejects(oldContinuation, /stale operator intent/);
|
|
assert.deepEqual(writes, []);
|
|
|
|
const assertFreshIntent = () => {
|
|
if (!generation.isOperatorIntentCurrent(freshIntent)) {
|
|
throw new Error("stale fresh intent");
|
|
}
|
|
};
|
|
await operatorIntentGeneration.awaitWhileIntentCurrent(
|
|
assertFreshIntent,
|
|
async () => ({ phase: "connection-ready" }),
|
|
);
|
|
writes.push("fresh-checkpoint");
|
|
assert.deepEqual(writes, ["fresh-checkpoint"]);
|
|
});
|
|
|
|
test("a fresh explicit K1 intent supersedes the previous intent in one runtime", () => {
|
|
const generation = new operatorIntentGeneration.OperatorIntentGeneration();
|
|
generation.activateRuntime();
|
|
const first = generation.beginOperatorIntent();
|
|
const second = generation.beginOperatorIntent();
|
|
|
|
assert.ok(first);
|
|
assert.ok(second);
|
|
assert.equal(first.runtimeGeneration, second.runtimeGeneration);
|
|
assert.notEqual(first.intentGeneration, second.intentGeneration);
|
|
assert.equal(generation.isOperatorIntentCurrent(first), false);
|
|
assert.equal(generation.isOperatorIntentCurrent(second), true);
|
|
});
|
|
|
|
test("canonical K1 launch wires the generation guard through every async stage", async () => {
|
|
const hookSource = await readFile(
|
|
new URL(
|
|
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
const canonicalStart = hookSource.slice(
|
|
hookSource.indexOf("const startCanonicalAcquisition"),
|
|
hookSource.indexOf("const prepareAcquisition"),
|
|
);
|
|
const pollingLoop = hookSource.slice(
|
|
hookSource.indexOf("async function waitForControlPhase"),
|
|
hookSource.indexOf("function messageFor"),
|
|
);
|
|
|
|
assert.doesNotMatch(hookSource, /mounted\.current/);
|
|
assert.match(canonicalStart, /beginOperatorIntent\(\)/);
|
|
assert.match(canonicalStart, /isOperatorIntentCurrent\(intentToken\)/);
|
|
assert.doesNotMatch(canonicalStart, /await xgridsK1Api\./);
|
|
assert.doesNotMatch(canonicalStart, /await waitForControlPhase\(/);
|
|
assert.ok(
|
|
canonicalStart.match(/awaitWhileIntentCurrent\(/g)?.length >= 8,
|
|
"each canonical REST/checkpoint boundary must use the intent guard",
|
|
);
|
|
assert.match(pollingLoop, /for \(;;\) \{\s*assertOperatorIntentCurrent\(\)/);
|
|
assert.equal(pollingLoop.match(/awaitWhileIntentCurrent\(/g)?.length, 2);
|
|
});
|
|
|
|
test("K1 control errors stay informative and only fetch failures mark transport unavailable", async () => {
|
|
assert.equal(
|
|
localizeRuntimeMessage(
|
|
"control MQTT connect call failed: [Errno 61] Connection refused",
|
|
),
|
|
"Управляющее соединение со сканером не открылось: устройство не приняло MQTT-соединение. Команды сканирования не отправлялись.",
|
|
);
|
|
|
|
const domainError = new ApiError("Диалог остановлен до команды START.");
|
|
assert.equal(domainError.transportUnavailable, false);
|
|
|
|
const originalFetch = globalThis.fetch;
|
|
globalThis.fetch = async () => {
|
|
throw new TypeError("synthetic network failure");
|
|
};
|
|
try {
|
|
await assert.rejects(
|
|
() => xgridsK1Api.getState(),
|
|
(error) => error instanceof ApiError && error.transportUnavailable === true,
|
|
);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
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");
|
|
assert.equal(lifecycle.liveStartPlan(acquiring), "already-running");
|
|
assert.equal(
|
|
lifecycle.controlSessionEntryPlan("scanning", false, false),
|
|
"continue",
|
|
);
|
|
});
|
|
|
|
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("project name is canonicalized and rejected outside the bounded safe contract", () => {
|
|
assert.deepEqual(projectName.validateProjectName(" Mission 01 "), {
|
|
value: "Mission 01",
|
|
error: null,
|
|
});
|
|
assert.match(projectName.validateProjectName("line\nbreak").error, /управляющие/);
|
|
assert.match(projectName.validateProjectName("\ud800").error, /управляющие/);
|
|
assert.match(projectName.validateProjectName("x".repeat(97)).error, /не длиннее 96/);
|
|
assert.match(projectName.validateProjectName(" \t ").error, /Введите название/);
|
|
});
|
|
|
|
test("automatic spatial source replaces the old scene only after a successful start", async () => {
|
|
const failedEvents = [];
|
|
assert.equal(await automaticSourceStart.runAutomaticSpatialSourceStart(
|
|
async () => {
|
|
failedEvents.push("start");
|
|
return false;
|
|
},
|
|
() => failedEvents.push("activate"),
|
|
() => failedEvents.push("open"),
|
|
), false);
|
|
assert.deepEqual(failedEvents, ["start"]);
|
|
|
|
const successfulEvents = [];
|
|
assert.equal(await automaticSourceStart.runAutomaticSpatialSourceStart(
|
|
async () => {
|
|
successfulEvents.push("start");
|
|
return true;
|
|
},
|
|
() => successfulEvents.push("activate"),
|
|
() => successfulEvents.push("open"),
|
|
), true);
|
|
assert.deepEqual(successfulEvents, ["start", "activate", "open"]);
|
|
});
|
|
|
|
test("vendor commands fail closed unless the profile and acquisition both enable them", () => {
|
|
const capability = {
|
|
compatibility: {
|
|
vendor_writes_enabled: true,
|
|
permitted_mode: "active-control",
|
|
},
|
|
};
|
|
assert.equal(lifecycle.isVendorWriteCapable(capability), true);
|
|
assert.equal(lifecycle.isVendorWriteCapable({
|
|
compatibility: { vendor_writes_enabled: true, permitted_mode: "read-only" },
|
|
}), false);
|
|
assert.equal(lifecycle.isSoftwareCommandedAcquisition({
|
|
...capability,
|
|
acquisition: { control_mode: "operator-manual" },
|
|
}), false);
|
|
assert.equal(lifecycle.isSoftwareCommandedAcquisition({
|
|
...capability,
|
|
acquisition: { control_mode: "plugin-commanded" },
|
|
}), true);
|
|
});
|
|
|
|
test("device modeling telemetry maps only finite non-negative values", () => {
|
|
assert.deepEqual(presentation.deviceTelemetry({
|
|
device_elapsed_seconds: 12.5,
|
|
device_route_distance_meters: 8.25,
|
|
device_speed_meters_per_second: 0.75,
|
|
}), {
|
|
elapsedSeconds: 12.5,
|
|
routeDistanceMeters: 8.25,
|
|
speedMetersPerSecond: 0.75,
|
|
});
|
|
assert.deepEqual(presentation.deviceTelemetry({
|
|
device_elapsed_seconds: -1,
|
|
device_route_distance_meters: Number.NaN,
|
|
device_speed_meters_per_second: Number.POSITIVE_INFINITY,
|
|
}), {
|
|
elapsedSeconds: null,
|
|
routeDistanceMeters: null,
|
|
speedMetersPerSecond: null,
|
|
});
|
|
});
|
|
|
|
test("spatial K1 action failures have an explicit retry-safe presentation", () => {
|
|
assert.equal(presentation.spatialActionFailure(null), null);
|
|
assert.equal(presentation.spatialActionFailure(" "), null);
|
|
assert.deepEqual(presentation.spatialActionFailure(" stop failed "), {
|
|
title: "Действие K1 не выполнено",
|
|
detail: "stop failed",
|
|
});
|
|
assert.equal(lifecycle.shouldRenderSpatialControls({
|
|
source_mode: "live",
|
|
acquisition: { state: "failed", cleanup_pending: true },
|
|
}), true);
|
|
assert.equal(lifecycle.shouldRenderSpatialControls({
|
|
source_mode: "idle",
|
|
acquisition: { state: "failed", cleanup_pending: false },
|
|
}), false);
|
|
});
|
|
|
|
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("K1 configuration exposes all reviewed local connection directions", () => {
|
|
assert.equal(configuration.DEFAULT_CONNECTION_MODE, "bridge");
|
|
assert.equal(configuration.SUPPORTED_MOUNT_TYPE, "handheld");
|
|
assert.equal(configuration.SUPPORTED_GNSS_MODE, "none");
|
|
assert.deepEqual(
|
|
configuration.connectionModeOptions.map(({ value, disabled = false }) => ({ value, disabled })),
|
|
[
|
|
{ value: "bridge", disabled: false },
|
|
{ value: "quick-connect", disabled: false },
|
|
{ value: "direct-connect", disabled: false },
|
|
],
|
|
);
|
|
assert.deepEqual(
|
|
configuration.mountTypeOptions.map(({ value, disabled = false }) => ({ value, disabled })),
|
|
[
|
|
{ value: "handheld", disabled: false },
|
|
{ value: "vehicle-mounted", disabled: true },
|
|
{ value: "uav", disabled: true },
|
|
{ value: "backpack", disabled: true },
|
|
],
|
|
);
|
|
assert.deepEqual(
|
|
configuration.gnssModeOptions.map(({ value, disabled = false }) => ({ value, disabled })),
|
|
[
|
|
{ value: "none", disabled: false },
|
|
{ value: "rtk", disabled: true },
|
|
{ value: "ppk", disabled: true },
|
|
],
|
|
);
|
|
});
|
|
|
|
test("connection directions select distinct fail-closed topologies", () => {
|
|
assert.deepEqual(compatibility.profileSelectionForConnectionMode("bridge"), {
|
|
firmware_version: "3.0.2",
|
|
topology: "direct-lan",
|
|
verification: "live-device-info",
|
|
});
|
|
assert.equal(
|
|
compatibility.profileSelectionForConnectionMode("quick-connect").topology,
|
|
"device-ap",
|
|
);
|
|
assert.equal(
|
|
compatibility.profileSelectionForConnectionMode("direct-connect").topology,
|
|
"controller-hotspot",
|
|
);
|
|
});
|
|
|
|
test("a provisioned address is not presented as a verified device connection", () => {
|
|
assert.equal(lifecycle.normalizeRuntimePhase({
|
|
phase: "connected",
|
|
application_control_session: { state: "idle" },
|
|
}), "configuring");
|
|
assert.equal(lifecycle.normalizeRuntimePhase({
|
|
phase: "connected",
|
|
application_control_session: { state: "connection-ready" },
|
|
}), "connected");
|
|
});
|
|
|
|
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",
|
|
verification: "live-device-info",
|
|
};
|
|
|
|
try {
|
|
await xgridsK1Api.connect({
|
|
device_id: "ble-device",
|
|
ssid: "lab-network",
|
|
password: syntheticCredential,
|
|
connection_mode: "bridge",
|
|
compatibility_attestation: attestation,
|
|
idempotency_key: "network-provision:test",
|
|
});
|
|
await xgridsK1Api.prepareAcquisition({
|
|
project_name: "Mission 01",
|
|
mount_type: "handheld",
|
|
gnss_mode: "none",
|
|
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);
|
|
assert.equal(prepare.input.project_name, "Mission 01");
|
|
});
|
|
|
|
test("connection verification supports refresh and explicit read-only adoption", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const calls = [];
|
|
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",
|
|
verification: "live-device-info",
|
|
};
|
|
|
|
try {
|
|
await xgridsK1Api.verifyConnection();
|
|
await xgridsK1Api.verifyConnection({
|
|
device_id: "fresh-ble-device",
|
|
compatibility_attestation: attestation,
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
|
|
assert.equal(calls.length, 2);
|
|
assert.match(String(calls[0].path), /actions\/connection\.verify$/);
|
|
assert.match(String(calls[1].path), /actions\/connection\.verify$/);
|
|
assert.deepEqual(JSON.parse(calls[0].init.body), { input: {} });
|
|
const adoption = JSON.parse(calls[1].init.body).input;
|
|
assert.deepEqual(adoption, {
|
|
device_id: "fresh-ble-device",
|
|
compatibility_attestation: attestation,
|
|
});
|
|
assert.equal("ssid" in adoption, false);
|
|
assert.equal("password" in adoption, false);
|
|
assert.equal("connection_mode" in adoption, false);
|
|
});
|
|
|
|
test("rejected network-profile writes use safe operator copy", () => {
|
|
const message = networkProvisionFailureMessage({
|
|
status: "failed",
|
|
error: { code: "BleakGATTProtocolError" },
|
|
});
|
|
|
|
assert.equal(
|
|
message,
|
|
"Сканер отклонил запись сетевого профиля. Результат изменения сети неизвестен; автоматический повтор запрещён. Проверьте текущее состояние K1 или подхватите существующее подключение без изменения настроек Wi‑Fi.",
|
|
);
|
|
assert.doesNotMatch(message, /Bleak|GATT|ATT/i);
|
|
});
|