Historical working copy retained for audit before consolidation into main. The canonicalized plugin architecture and later fixes already live in main; this snapshot is not a release or a request to restore obsolete source layout.
4259 lines
145 KiB
JavaScript
4259 lines
145 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;
|
||
let awaitNetworkProvisionSettlementAfterLostResponse;
|
||
let discoveryScanFailureMessage;
|
||
let connectionVerificationFailureMessage;
|
||
let operationById;
|
||
let physicalCommandConfirmation;
|
||
let controlSessionCas;
|
||
let ApiRequestTimeoutError;
|
||
let requestTimeoutMsForAction;
|
||
let isXgridsConnectionVerification;
|
||
let isXgridsConnectionReconfiguration;
|
||
let isXgridsConnectionPolicy;
|
||
let isXgridsConnectionPolicyDecision;
|
||
let isXgridsHostFailureDiagnostic;
|
||
let XGRIDS_CONNECTION_VERIFICATION_STATUSES;
|
||
let XGRIDS_CONNECTION_VERIFICATION_LEASE_STATES;
|
||
let XGRIDS_CONNECTION_POLICY_ACTIONS;
|
||
let XGRIDS_CONNECTION_POLICY_TARGET_SOURCES;
|
||
let XGRIDS_CONNECTION_ATTEMPT_PHASES;
|
||
let isXgridsConnectionAttemptPhase;
|
||
|
||
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,
|
||
ApiRequestTimeoutError,
|
||
requestTimeoutMsForAction,
|
||
isXgridsConnectionVerification,
|
||
isXgridsConnectionReconfiguration,
|
||
isXgridsConnectionPolicy,
|
||
isXgridsConnectionPolicyDecision,
|
||
isXgridsHostFailureDiagnostic,
|
||
XGRIDS_CONNECTION_VERIFICATION_STATUSES,
|
||
XGRIDS_CONNECTION_VERIFICATION_LEASE_STATES,
|
||
XGRIDS_CONNECTION_POLICY_ACTIONS,
|
||
XGRIDS_CONNECTION_POLICY_TARGET_SOURCES,
|
||
XGRIDS_CONNECTION_ATTEMPT_PHASES,
|
||
isXgridsConnectionAttemptPhase,
|
||
} = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/api.ts",
|
||
));
|
||
({ localizeRuntimeMessage } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/messages.ts",
|
||
));
|
||
({
|
||
networkProvisionFailureMessage,
|
||
awaitNetworkProvisionSettlementAfterLostResponse,
|
||
discoveryScanFailureMessage,
|
||
connectionVerificationFailureMessage,
|
||
operationById,
|
||
} = await server.ssrLoadModule("@xgrids-k1/frontend/useXgridsK1Runtime.ts"));
|
||
physicalCommandConfirmation = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/physicalCommandConfirmation.ts",
|
||
);
|
||
controlSessionCas = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/controlSessionCas.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 },
|
||
};
|
||
}
|
||
|
||
function supervisedConnectionState({
|
||
mode = "bridge",
|
||
leaseState = "reachable",
|
||
generation = 1,
|
||
intentId = `intent-${mode}-1`,
|
||
hostPathEpoch = 3,
|
||
target = mode === "quick-connect"
|
||
? { ipv4: "192.168.56.1", port: 1883 }
|
||
: { ipv4: "192.168.68.50", port: 1883 },
|
||
controlAllowed = leaseState === "reachable",
|
||
dataAuthoritative = false,
|
||
lastKnown = null,
|
||
deviceNetworkState = "applied",
|
||
hostAvailable = true,
|
||
endpointState = "reachable",
|
||
} = {}) {
|
||
const observedAt = "2026-08-06T12:00:00Z";
|
||
const identityVerified = leaseState === "reachable";
|
||
const connectionReady = Boolean(
|
||
controlAllowed
|
||
&& identityVerified
|
||
&& leaseState === "reachable"
|
||
&& deviceNetworkState === "applied"
|
||
&& hostAvailable
|
||
&& endpointState === "reachable",
|
||
);
|
||
return {
|
||
snapshot_runtime_id: "runtime-a",
|
||
network_write_reconciliation: null,
|
||
connection_lifecycle: {
|
||
schema_version: "missioncore.xgrids-k1-connection-lifecycle/v1",
|
||
revision: generation,
|
||
desired_mode: mode,
|
||
configured_mode: deviceNetworkState === "applied" ? mode : null,
|
||
active_mode: connectionReady ? mode : null,
|
||
mode_change: {
|
||
state: connectionReady ? "ready" : "awaiting-control",
|
||
from: deviceNetworkState === "applied" ? mode : null,
|
||
to: mode,
|
||
},
|
||
mode_selection: {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
automatic_retry: false,
|
||
},
|
||
active_binding_key: connectionReady ? `binding-${intentId}-${hostPathEpoch}` : null,
|
||
active_binding: connectionReady
|
||
? {
|
||
binding_key: `binding-${intentId}-${hostPathEpoch}`,
|
||
intent_id: intentId,
|
||
transport_ref: "ble-k1-001",
|
||
connection_mode: mode,
|
||
target_ipv4: target.ipv4,
|
||
target_port: target.port,
|
||
host_path_epoch: hostPathEpoch,
|
||
control_session_id: "control-session-001",
|
||
logical_device_id: "device-k1-001",
|
||
}
|
||
: null,
|
||
connection_ready: connectionReady,
|
||
ready_to_start: connectionReady,
|
||
operation: null,
|
||
allowed_actions: connectionReady
|
||
? ["start-acquisition", "select-connection-mode"]
|
||
: ["select-connection-mode"],
|
||
automatic_retry: false,
|
||
},
|
||
connection_supervisor: {
|
||
schema_version: "missioncore.k1-connection-supervisor/v1",
|
||
revision: generation,
|
||
closed: false,
|
||
intent: {
|
||
intent_id: intentId,
|
||
requested_mode: mode,
|
||
expected_device_id: "device-k1-001",
|
||
requested_at: observedAt,
|
||
},
|
||
observed: {
|
||
device_network: {
|
||
state: deviceNetworkState,
|
||
intent_id: deviceNetworkState === "applied" ? intentId : null,
|
||
transport_ref: deviceNetworkState === "applied" ? "ble-k1-001" : null,
|
||
connection_mode: deviceNetworkState === "applied" ? mode : null,
|
||
target: deviceNetworkState === "applied" ? target : null,
|
||
source: deviceNetworkState === "applied" ? "ble-read-only-status" : null,
|
||
observed_at: deviceNetworkState === "applied" ? observedAt : null,
|
||
},
|
||
host_path: {
|
||
epoch: hostPathEpoch,
|
||
available: hostAvailable,
|
||
fingerprint: `en0:${hostPathEpoch}`,
|
||
interface: "en0",
|
||
source_ipv4: "192.168.68.10",
|
||
route_class: hostAvailable ? "direct" : "unavailable",
|
||
reason_code: null,
|
||
observed_at: observedAt,
|
||
},
|
||
endpoint: {
|
||
target,
|
||
tcp_state: endpointState,
|
||
intent_id: intentId,
|
||
host_path_epoch: hostPathEpoch,
|
||
reason_code: null,
|
||
observed_at: observedAt,
|
||
},
|
||
device_identity: {
|
||
state: identityVerified ? "verified" : "unverified",
|
||
intent_id: identityVerified ? intentId : null,
|
||
logical_device_id: identityVerified ? "device-k1-001" : null,
|
||
compatibility_profile_id: identityVerified
|
||
? "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2"
|
||
: null,
|
||
connection_mode: identityVerified ? mode : null,
|
||
source: identityVerified ? "mqtt-device-info" : null,
|
||
host_path_epoch: identityVerified ? hostPathEpoch : null,
|
||
observed_at: identityVerified ? observedAt : null,
|
||
},
|
||
control_plane: {
|
||
state: controlAllowed ? "healthy" : "idle",
|
||
session_id: controlAllowed ? "control-session-001" : null,
|
||
host_path_epoch: controlAllowed ? hostPathEpoch : null,
|
||
reason_code: null,
|
||
observed_at: controlAllowed ? observedAt : null,
|
||
},
|
||
data_plane: {
|
||
state: dataAuthoritative ? "healthy" : "idle",
|
||
session_id: dataAuthoritative ? "data-session-001" : null,
|
||
host_path_epoch: dataAuthoritative ? hostPathEpoch : null,
|
||
reason_code: null,
|
||
observed_at: dataAuthoritative ? observedAt : null,
|
||
},
|
||
},
|
||
lease: {
|
||
state: leaseState,
|
||
generation,
|
||
intent_id: intentId,
|
||
host_path_epoch: hostPathEpoch,
|
||
connection_mode: mode,
|
||
target,
|
||
logical_device_id: identityVerified ? "device-k1-001" : null,
|
||
reason_code: null,
|
||
observed_at: observedAt,
|
||
},
|
||
authority: {
|
||
network_mutation_allowed: false,
|
||
control_allowed: controlAllowed,
|
||
acquisition_start_allowed: controlAllowed,
|
||
data_ingest_authoritative: dataAuthoritative,
|
||
physical_motion_allowed: false,
|
||
reason_codes: [],
|
||
},
|
||
last_known: lastKnown,
|
||
allowed_actions: [],
|
||
},
|
||
};
|
||
}
|
||
|
||
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");
|
||
assert.equal(
|
||
xgridsK1Actions.configuredEndpointProbe,
|
||
"connection.endpoint-probe",
|
||
);
|
||
});
|
||
|
||
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("canonical K1 intent cannot cross from snapshot runtime A into B", async () => {
|
||
const generation = new operatorIntentGeneration.OperatorIntentGeneration();
|
||
generation.activateRuntime();
|
||
const intent = generation.beginOperatorIntent();
|
||
assert.ok(intent);
|
||
|
||
const actionRuntimeId = "snapshot-runtime-a";
|
||
let currentRuntimeId = actionRuntimeId;
|
||
const writes = [];
|
||
const assertActionCurrent = () => {
|
||
if (
|
||
!generation.isOperatorIntentCurrent(intent)
|
||
|| !operatorIntentGeneration.isSnapshotRuntimeCurrent(
|
||
actionRuntimeId,
|
||
currentRuntimeId,
|
||
)
|
||
) {
|
||
throw new Error("snapshot runtime changed");
|
||
}
|
||
};
|
||
|
||
const continuation = operatorIntentGeneration.awaitWhileIntentCurrent(
|
||
assertActionCurrent,
|
||
async () => {
|
||
currentRuntimeId = "snapshot-runtime-b";
|
||
return { phase: "connection-ready" };
|
||
},
|
||
).then(() => writes.push("next-mutation"));
|
||
|
||
await assert.rejects(continuation, /snapshot runtime changed/);
|
||
assert.deepEqual(writes, []);
|
||
});
|
||
|
||
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 preparation stops before START and guards every async stage", async () => {
|
||
const hookSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
const canonicalPreparation = hookSource.slice(
|
||
hookSource.indexOf("const prepareCanonicalAcquisition"),
|
||
hookSource.indexOf("const prepareAcquisition"),
|
||
);
|
||
const finalStart = hookSource.slice(
|
||
hookSource.indexOf("const startPreparedAcquisition"),
|
||
hookSource.indexOf("const startReplay"),
|
||
);
|
||
const controlPollingLoop = hookSource.slice(
|
||
hookSource.indexOf("async function waitForControlPhase"),
|
||
hookSource.indexOf("async function waitForPhysicalReconciliationProof"),
|
||
);
|
||
const reconciliationPollingLoop = hookSource.slice(
|
||
hookSource.indexOf("async function waitForPhysicalReconciliationProof"),
|
||
hookSource.indexOf("function messageFor"),
|
||
);
|
||
|
||
assert.doesNotMatch(hookSource, /mounted\.current/);
|
||
assert.match(canonicalPreparation, /beginOperatorIntent\(\)/);
|
||
assert.match(canonicalPreparation, /isOperatorIntentCurrent\(intentToken\)/);
|
||
assert.match(
|
||
canonicalPreparation,
|
||
/const actionSnapshotRuntimeId = expectedSnapshotRuntimeId\(\)/,
|
||
);
|
||
assert.match(
|
||
canonicalPreparation,
|
||
/isSnapshotRuntimeCurrent\(actionSnapshotRuntimeId\)/,
|
||
);
|
||
assert.equal(
|
||
canonicalPreparation.match(
|
||
/expected_snapshot_runtime_id: actionSnapshotRuntimeId/g,
|
||
)?.length,
|
||
4,
|
||
);
|
||
assert.match(canonicalPreparation, /xgridsK1Api\.openApplicationControlSession\(\{/);
|
||
assert.match(canonicalPreparation, /\.\.\.request\.physicalAcceptance/);
|
||
assert.ok(
|
||
canonicalPreparation.indexOf("xgridsK1Api.openApplicationControlSession")
|
||
< canonicalPreparation.indexOf("xgridsK1Api.enterApplicationWorkspace"),
|
||
"Verify inspection must be replaced before workspace entry",
|
||
);
|
||
assert.match(canonicalPreparation, /xgridsK1Api\.reconcilePhysicalCommand\(\{/);
|
||
assert.doesNotMatch(canonicalPreparation, /await xgridsK1Api\./);
|
||
assert.doesNotMatch(canonicalPreparation, /await waitForControlPhase\(/);
|
||
assert.doesNotMatch(canonicalPreparation, /xgridsK1Api\.startAcquisition/);
|
||
assert.match(finalStart, /xgridsK1Api\.startAcquisition/);
|
||
assert.ok(
|
||
canonicalPreparation.match(/awaitWhileIntentCurrent\(/g)?.length >= 7,
|
||
"each preparation REST/checkpoint boundary must use the intent guard",
|
||
);
|
||
assert.match(controlPollingLoop, /for \(;;\) \{\s*assertOperatorIntentCurrent\(\)/);
|
||
assert.equal(controlPollingLoop.match(/awaitWhileIntentCurrent\(/g)?.length, 2);
|
||
assert.match(
|
||
reconciliationPollingLoop,
|
||
/for \(;;\) \{\s*assertOperatorIntentCurrent\(\)/,
|
||
);
|
||
assert.equal(
|
||
reconciliationPollingLoop.match(/awaitWhileIntentCurrent\(/g)?.length,
|
||
2,
|
||
);
|
||
});
|
||
|
||
test("K1 control-phase polling fails closed instead of stacking timed-out state reads", async () => {
|
||
const hookSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
const controlPollingLoop = hookSource.slice(
|
||
hookSource.indexOf("async function waitForControlPhase"),
|
||
hookSource.indexOf("async function waitForPhysicalReconciliationProof"),
|
||
);
|
||
|
||
assert.match(hookSource, /const CONTROL_PHASE_WAIT_TIMEOUT_MS = 120_000/);
|
||
assert.match(controlPollingLoop, /xgridsK1Api\.getState\(\)/);
|
||
assert.doesNotMatch(controlPollingLoop, /ApiRequestTimeoutError/);
|
||
assert.doesNotMatch(controlPollingLoop, /catch \(error\)/);
|
||
assert.doesNotMatch(
|
||
controlPollingLoop,
|
||
/(?:openApplicationControlSession|enterApplicationWorkspace|prepareAcquisition|startAcquisition|stopAcquisition)\(/,
|
||
);
|
||
});
|
||
|
||
test("K1 frontend coalesces concurrent read-only state requests without caching them", async () => {
|
||
const apiSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/api.ts",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
const singleFlight = apiSource.slice(
|
||
apiSource.indexOf("let stateReadInFlight"),
|
||
apiSource.indexOf("export const xgridsK1Api"),
|
||
);
|
||
const getState = apiSource.slice(
|
||
apiSource.indexOf("async getState"),
|
||
apiSource.indexOf("scanBle("),
|
||
);
|
||
|
||
assert.match(singleFlight, /if \(stateReadInFlight\) return stateReadInFlight/);
|
||
assert.match(singleFlight, /invokeState\(xgridsK1Actions\.stateRead\)/);
|
||
assert.match(singleFlight, /stateReadInFlight === request[\s\S]*?stateReadInFlight = null/);
|
||
assert.match(getState, /return readStateSingleFlight\(\)/);
|
||
assert.doesNotMatch(singleFlight, /setTimeout|setInterval|retry/i);
|
||
});
|
||
|
||
test("K1 control-session CAS is exact, integer-only and mapped for each mutation family", () => {
|
||
const state = {
|
||
application_control_session: {
|
||
mode: "interactive-canonical",
|
||
state: "workspace-ready",
|
||
session_generation: 17,
|
||
state_revision: 43,
|
||
},
|
||
};
|
||
assert.deepEqual(
|
||
controlSessionCas.exactApplicationControlCas(state, "ENTER"),
|
||
{
|
||
expected_session_generation: 17,
|
||
expected_state_revision: 43,
|
||
},
|
||
);
|
||
assert.deepEqual(
|
||
controlSessionCas.exactAcquisitionControlCas(state, "PREPARE"),
|
||
{
|
||
expected_control_session_generation: 17,
|
||
expected_control_state_revision: 43,
|
||
},
|
||
);
|
||
assert.equal(controlSessionCas.acquisitionMutationUsesControlSession(state), true);
|
||
assert.equal(
|
||
controlSessionCas.acquisitionMutationUsesControlSession({
|
||
application_control_session: { ...state.application_control_session, state: "closed" },
|
||
}),
|
||
false,
|
||
);
|
||
|
||
for (const [field, value] of [
|
||
["session_generation", undefined],
|
||
["session_generation", 1.5],
|
||
["session_generation", -1],
|
||
["session_generation", Number.NaN],
|
||
["state_revision", undefined],
|
||
["state_revision", 2.25],
|
||
["state_revision", -1],
|
||
["state_revision", Number.POSITIVE_INFINITY],
|
||
]) {
|
||
assert.throws(
|
||
() => controlSessionCas.exactApplicationControlCas({
|
||
application_control_session: {
|
||
...state.application_control_session,
|
||
[field]: value,
|
||
},
|
||
}, "TEST"),
|
||
(error) => error instanceof ApiError && /не отправлена/.test(error.message),
|
||
`${field}=${String(value)}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test("K1 frontend reads CAS only from latest accepted state immediately before API mutations", async () => {
|
||
const apiSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/api.ts",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
const hookSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
|
||
assert.match(apiSource, /session_generation:\s*number/);
|
||
assert.match(apiSource, /state_revision:\s*number/);
|
||
assert.match(apiSource, /expected_session_generation:\s*number/);
|
||
assert.match(apiSource, /expected_control_session_generation:\s*number/);
|
||
assert.match(apiSource, /closeApplicationControlSession\(\s*body:/);
|
||
assert.doesNotMatch(hookSource, /closeApplicationControlSession\(\)/);
|
||
assert.ok(
|
||
[...hookSource.matchAll(/exactApplicationControlCas\(\s*latestState\.current/g)].length >= 3,
|
||
"standalone ENTER, CLOSE and canonical ENTER must use latest accepted state",
|
||
);
|
||
assert.ok(
|
||
[...hookSource.matchAll(/exactAcquisitionControlCas\(\s*latestState\.current/g)].length >= 4,
|
||
"canonical PREPARE, generic PREPARE, START and STOP must use latest accepted state",
|
||
);
|
||
assert.match(
|
||
hookSource,
|
||
/nextState = latestState\.current \?\? nextState;[\s\S]*?const phase = controlPhase\(nextState\)/,
|
||
);
|
||
assert.match(
|
||
hookSource,
|
||
/mode:\s*"graceful",[\s\S]*?\.\.\.newMutationContext\("acquisition\.stop"\),[\s\S]*?\.\.\.controlCas/,
|
||
);
|
||
assert.match(hookSource, /mode:\s*"capture-only"/);
|
||
|
||
const stopMutation = hookSource.slice(
|
||
hookSource.indexOf("const stop = useCallback"),
|
||
hookSource.indexOf("const stopLocalReceiver = useCallback"),
|
||
);
|
||
const localReceiverCleanup = hookSource.slice(
|
||
hookSource.indexOf("const stopLocalReceiver = useCallback"),
|
||
hookSource.indexOf("const abort = useCallback"),
|
||
);
|
||
const mutationGate = stopMutation.indexOf(
|
||
"physicalStopIntentCheckpoint(dispatchState)",
|
||
);
|
||
const exactCas = stopMutation.indexOf("exactAcquisitionControlCas(", mutationGate);
|
||
const spentFence = stopMutation.indexOf("spendPhysicalStopIntent(checkpoint)", exactCas);
|
||
const physicalCall = stopMutation.indexOf("xgridsK1Api.stopAcquisition({", spentFence);
|
||
assert.ok(
|
||
mutationGate >= 0
|
||
&& exactCas > mutationGate
|
||
&& spentFence > exactCas
|
||
&& physicalCall > spentFence,
|
||
"physical STOP must re-read exact authority and spend it synchronously before its API call",
|
||
);
|
||
assert.match(stopMutation, /physicalStopPresentationOwner\.current === stopPresentationOwner/);
|
||
const captureOnlyMarker = stopMutation.indexOf('mode: "capture-only"');
|
||
const captureOnlyPolicyGate = stopMutation.lastIndexOf(
|
||
'connectionPolicyAllows(currentState, "stop-local-receiver")',
|
||
captureOnlyMarker,
|
||
);
|
||
const compatibilityCall = stopMutation.indexOf(
|
||
"xgridsK1Api.stopSessionCompatibility({",
|
||
captureOnlyMarker,
|
||
);
|
||
const compatibilityPolicyGate = stopMutation.lastIndexOf(
|
||
'connectionPolicyAllows(currentState, "stop-local-receiver")',
|
||
compatibilityCall,
|
||
);
|
||
assert.ok(
|
||
captureOnlyPolicyGate >= 0
|
||
&& captureOnlyPolicyGate < captureOnlyMarker
|
||
&& compatibilityPolicyGate > captureOnlyMarker
|
||
&& compatibilityPolicyGate < compatibilityCall,
|
||
"generic capture-only and compatibility STOP must fail closed on the current local-stop policy before API dispatch",
|
||
);
|
||
const localReceiverPolicyGate = localReceiverCleanup.indexOf(
|
||
'connectionPolicyAllows(currentState, "stop-local-receiver")',
|
||
);
|
||
const localReceiverFirstApiCall = localReceiverCleanup.indexOf("xgridsK1Api.");
|
||
assert.ok(
|
||
localReceiverPolicyGate >= 0
|
||
&& localReceiverPolicyGate < localReceiverFirstApiCall,
|
||
"a direct or stale local cleanup call must be denied before either API path",
|
||
);
|
||
assert.match(
|
||
localReceiverCleanup,
|
||
/const stopPlan = localReceiverStopPlan\(currentState\)/,
|
||
);
|
||
assert.match(
|
||
localReceiverCleanup,
|
||
/stopPlan\.kind === "acquisition"/,
|
||
);
|
||
assert.match(
|
||
localReceiverCleanup,
|
||
/xgridsK1Api\.stopAcquisition\(\{[\s\S]*?acquisition_id: stopPlan\.acquisitionId,[\s\S]*?mode: "capture-only"[\s\S]*?expected_snapshot_runtime_id: snapshotRuntimeId/,
|
||
);
|
||
assert.match(
|
||
localReceiverCleanup,
|
||
/xgridsK1Api\.stopSessionCompatibility\(\{[\s\S]*?expected_snapshot_runtime_id: snapshotRuntimeId/,
|
||
);
|
||
assert.doesNotMatch(
|
||
localReceiverCleanup,
|
||
/exactAcquisitionControlCas|physical_acceptance|spendPhysicalStopIntent/,
|
||
);
|
||
});
|
||
|
||
test("K1 API forwards runtime fences and exact CAS fields without dropping them", 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" },
|
||
});
|
||
};
|
||
|
||
try {
|
||
await xgridsK1Api.openApplicationControlSession({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operator_present: true,
|
||
owner_controlled_device: true,
|
||
lixelgo_closed: true,
|
||
battery_storage_confirmed: true,
|
||
expected_physical_state_confirmed: true,
|
||
timezone_name: "Europe/Moscow",
|
||
});
|
||
await xgridsK1Api.enterApplicationWorkspace({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operator_confirmed: true,
|
||
expected_session_generation: 7,
|
||
expected_state_revision: 8,
|
||
});
|
||
await xgridsK1Api.closeApplicationControlSession({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
expected_session_generation: 9,
|
||
expected_state_revision: 10,
|
||
});
|
||
await xgridsK1Api.reconcilePhysicalCommand({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
reconciliation_id: "reconciliation-cas",
|
||
expected_session_generation: 9,
|
||
expected_state_revision: 10,
|
||
});
|
||
await xgridsK1Api.prepareAcquisition({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operation_id: "op-00000000-0000-4000-8000-000000000101",
|
||
idempotency_key: "acquisition.prepare:op-00000000-0000-4000-8000-000000000101",
|
||
project_name: "CAS01",
|
||
mount_type: "handheld",
|
||
gnss_mode: "none",
|
||
compatibility_attestation: {
|
||
firmware_version: "3.0.2",
|
||
topology: "direct-lan",
|
||
verification: "live-device-info",
|
||
},
|
||
expected_control_session_generation: 11,
|
||
expected_control_state_revision: 12,
|
||
});
|
||
await xgridsK1Api.startAcquisition({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operation_id: "op-00000000-0000-4000-8000-000000000102",
|
||
idempotency_key: "acquisition.start:op-00000000-0000-4000-8000-000000000102",
|
||
acquisition_id: "acquisition-cas",
|
||
expected_control_session_generation: 13,
|
||
expected_control_state_revision: 14,
|
||
});
|
||
await xgridsK1Api.stopAcquisition({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operation_id: "op-00000000-0000-4000-8000-000000000103",
|
||
idempotency_key: "acquisition.stop:op-00000000-0000-4000-8000-000000000103",
|
||
acquisition_id: "acquisition-cas",
|
||
mode: "graceful",
|
||
expected_control_session_generation: 15,
|
||
expected_control_state_revision: 16,
|
||
});
|
||
await xgridsK1Api.abortAcquisition({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operation_id: "op-00000000-0000-4000-8000-000000000104",
|
||
idempotency_key: "acquisition.abort:op-00000000-0000-4000-8000-000000000104",
|
||
acquisition_id: "acquisition-cas",
|
||
expected_control_session_generation: 17,
|
||
expected_control_state_revision: 18,
|
||
});
|
||
await xgridsK1Api.stopSessionCompatibility({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
});
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
|
||
assert.deepEqual(
|
||
calls.map(({ init }) => JSON.parse(init.body).input),
|
||
[
|
||
{
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operator_present: true,
|
||
owner_controlled_device: true,
|
||
lixelgo_closed: true,
|
||
battery_storage_confirmed: true,
|
||
expected_physical_state_confirmed: true,
|
||
timezone_name: "Europe/Moscow",
|
||
},
|
||
{
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operator_confirmed: true,
|
||
expected_session_generation: 7,
|
||
expected_state_revision: 8,
|
||
},
|
||
{
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
expected_session_generation: 9,
|
||
expected_state_revision: 10,
|
||
},
|
||
{
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
reconciliation_id: "reconciliation-cas",
|
||
expected_session_generation: 9,
|
||
expected_state_revision: 10,
|
||
},
|
||
{
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operation_id: "op-00000000-0000-4000-8000-000000000101",
|
||
idempotency_key: "acquisition.prepare:op-00000000-0000-4000-8000-000000000101",
|
||
project_name: "CAS01",
|
||
mount_type: "handheld",
|
||
gnss_mode: "none",
|
||
compatibility_attestation: {
|
||
firmware_version: "3.0.2",
|
||
topology: "direct-lan",
|
||
verification: "live-device-info",
|
||
},
|
||
expected_control_session_generation: 11,
|
||
expected_control_state_revision: 12,
|
||
},
|
||
{
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operation_id: "op-00000000-0000-4000-8000-000000000102",
|
||
idempotency_key: "acquisition.start:op-00000000-0000-4000-8000-000000000102",
|
||
acquisition_id: "acquisition-cas",
|
||
expected_control_session_generation: 13,
|
||
expected_control_state_revision: 14,
|
||
},
|
||
{
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operation_id: "op-00000000-0000-4000-8000-000000000103",
|
||
idempotency_key: "acquisition.stop:op-00000000-0000-4000-8000-000000000103",
|
||
acquisition_id: "acquisition-cas",
|
||
mode: "graceful",
|
||
expected_control_session_generation: 15,
|
||
expected_control_state_revision: 16,
|
||
},
|
||
{
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
operation_id: "op-00000000-0000-4000-8000-000000000104",
|
||
idempotency_key: "acquisition.abort:op-00000000-0000-4000-8000-000000000104",
|
||
acquisition_id: "acquisition-cas",
|
||
expected_control_session_generation: 17,
|
||
expected_control_state_revision: 18,
|
||
},
|
||
{
|
||
expected_snapshot_runtime_id: "snapshot-runtime-cas",
|
||
},
|
||
],
|
||
);
|
||
});
|
||
|
||
test("K1 lifecycle mutation context is unique and binds one key to one operation", () => {
|
||
const first = lifecycle.newMutationContext("acquisition.start");
|
||
const second = lifecycle.newMutationContext("acquisition.start");
|
||
|
||
assert.match(first.operation_id, /^op-[0-9a-f-]{36}$/);
|
||
assert.equal(first.idempotency_key, `acquisition.start:${first.operation_id}`);
|
||
assert.notEqual(second.operation_id, first.operation_id);
|
||
assert.equal(second.idempotency_key, `acquisition.start:${second.operation_id}`);
|
||
assert.throws(
|
||
() => lifecycle.newMutationContext(" "),
|
||
/безопасный ключ не создан/,
|
||
);
|
||
});
|
||
|
||
test("K1 API uses bounded no-retry timeout classes per operation family", async () => {
|
||
const apiSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/api.ts",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
assert.equal(requestTimeoutMsForAction(xgridsK1Actions.stateRead), 8_000);
|
||
assert.equal(requestTimeoutMsForAction(xgridsK1Actions.discoveryScan), 70_000);
|
||
assert.equal(requestTimeoutMsForAction(xgridsK1Actions.networkProvision), 300_000);
|
||
assert.equal(requestTimeoutMsForAction(xgridsK1Actions.connectionVerify), 150_000);
|
||
assert.equal(requestTimeoutMsForAction(xgridsK1Actions.configuredEndpointProbe), 15_000);
|
||
for (const action of [
|
||
xgridsK1Actions.applicationControlSessionOpen,
|
||
xgridsK1Actions.applicationControlWorkspaceEnter,
|
||
xgridsK1Actions.applicationControlSessionClose,
|
||
xgridsK1Actions.acquisitionPrepare,
|
||
xgridsK1Actions.acquisitionStart,
|
||
xgridsK1Actions.acquisitionStop,
|
||
]) {
|
||
assert.equal(requestTimeoutMsForAction(action), 120_000, action);
|
||
}
|
||
const timeout = new ApiRequestTimeoutError("acquisition.start", 120_000);
|
||
assert.equal(timeout.outcomeUnknown, true);
|
||
assert.equal(timeout.automaticRetry, false);
|
||
assert.equal(timeout.transportUnavailable, false);
|
||
assert.match(timeout.message, /автоматический повтор запрещён/);
|
||
assert.match(apiSource, /const controller = new AbortController\(\)/);
|
||
assert.match(apiSource, /setTimeout\(\(\) => \{\s*timedOut = true;\s*controller\.abort\(\)/);
|
||
assert.match(apiSource, /finally \{\s*clearTimeout\(timeout\)/);
|
||
assert.doesNotMatch(apiSource, /automaticRetry\s*=\s*true/);
|
||
});
|
||
|
||
test("lost Quick response follows the admitted Apply by journal reads only", async () => {
|
||
const idempotencyKey = "network-provision:quick-lost-response";
|
||
let nowMs = 1_000;
|
||
let reads = 0;
|
||
const accepted = [];
|
||
const operation = {
|
||
operation_id: "op-quick-lost-response",
|
||
action: "network.provision",
|
||
status: "running",
|
||
idempotency_key: idempotencyKey,
|
||
deadline_at: new Date(5_000).toISOString(),
|
||
};
|
||
const initialState = {
|
||
snapshot_runtime_id: "runtime-quick-lost-response",
|
||
operations: [operation],
|
||
};
|
||
const terminalState = {
|
||
...initialState,
|
||
operations: [{
|
||
...operation,
|
||
status: "succeeded",
|
||
result: { phase: "network_applied" },
|
||
}],
|
||
};
|
||
|
||
const settled = await awaitNetworkProvisionSettlementAfterLostResponse(
|
||
initialState,
|
||
idempotencyKey,
|
||
async () => {
|
||
reads += 1;
|
||
if (reads === 1) {
|
||
throw new ApiError("transient localhost handoff", 0, true);
|
||
}
|
||
return terminalState;
|
||
},
|
||
(state) => accepted.push(state),
|
||
() => {},
|
||
{
|
||
now: () => nowMs,
|
||
wait: async (delayMs) => {
|
||
nowMs += delayMs;
|
||
},
|
||
},
|
||
);
|
||
|
||
assert.equal(reads, 2);
|
||
assert.deepEqual(accepted, [terminalState]);
|
||
assert.equal(settled.operations[0].status, "succeeded");
|
||
});
|
||
|
||
test("one explicit K1 operator action supplies the backend physical acceptance", () => {
|
||
assert.deepEqual(physicalCommandConfirmation.operatorActionPhysicalAcceptance(), {
|
||
operator_present: true,
|
||
owner_controlled_device: true,
|
||
lixelgo_closed: true,
|
||
battery_storage_confirmed: true,
|
||
expected_physical_state_confirmed: true,
|
||
});
|
||
});
|
||
|
||
test("final K1 START target requires exact prepared project and fresh READY authority", () => {
|
||
const state = {
|
||
...supervisedConnectionState(),
|
||
phase: "ready",
|
||
source_mode: "idle",
|
||
selected_device_id: "ble-k1-001",
|
||
device_ref: {
|
||
device_id: "device-k1-001",
|
||
},
|
||
device_session: {
|
||
device_session_id: "device-session-001",
|
||
device_id: "device-k1-001",
|
||
connectivity: "connected",
|
||
},
|
||
compatibility: {
|
||
permitted_mode: "active-control",
|
||
vendor_writes_enabled: true,
|
||
},
|
||
acquisition: {
|
||
acquisition_id: "acquisition-001",
|
||
device_id: "device-k1-001",
|
||
device_session_id: "device-session-001",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
control_mode: "plugin-commanded",
|
||
project_name: "RAVNOVES01",
|
||
requested_streams: [],
|
||
target_host: "127.0.0.1",
|
||
duration_seconds: null,
|
||
evidence_policy: "required",
|
||
state: "prepared",
|
||
state_revision: 4,
|
||
},
|
||
application_control_session: {
|
||
session_generation: 5,
|
||
state_revision: 8,
|
||
state: "project-ready",
|
||
control_socket_open: true,
|
||
can_start: true,
|
||
verified_control: {
|
||
logical_device_id: "device-k1-001",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
control_session_id: "control-session-001",
|
||
source: "mqtt-device-info",
|
||
intent_id: "intent-bridge-1",
|
||
transport_ref: "ble-k1-001",
|
||
host_path_epoch: 3,
|
||
target_ipv4: "192.168.68.50",
|
||
target_port: 1883,
|
||
connection_mode: "bridge",
|
||
control_proof_revision: 9,
|
||
control_proof_source: "device-status",
|
||
control_proof_fresh: true,
|
||
},
|
||
transport: {
|
||
latest_device_session_state: "ready",
|
||
latest_device_project_bound: true,
|
||
latest_device_init_ready: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
const target = physicalCommandConfirmation.preparedStartTarget(state);
|
||
assert.ok(target);
|
||
const { fence, ...displayTarget } = target;
|
||
assert.deepEqual(displayTarget, {
|
||
deviceId: "device-k1-001",
|
||
connection: "bridge · 192.168.68.50:1883",
|
||
projectName: "RAVNOVES01",
|
||
acquisitionId: "acquisition-001",
|
||
deviceState: "READY · проект привязан · инициализация не запущена",
|
||
});
|
||
assert.equal(fence.kind, "start");
|
||
assert.equal(fence.controlSessionGeneration, 5);
|
||
assert.equal(fence.controlStateRevision, 8);
|
||
assert.equal(fence.connectionIntentId, "intent-bridge-1");
|
||
assert.equal(fence.transportRef, "ble-k1-001");
|
||
assert.equal(fence.acquisitionStateRevision, 4);
|
||
|
||
for (const mutate of [
|
||
(candidate) => { candidate.application_control_session.can_start = false; },
|
||
(candidate) => { candidate.application_control_session.transport.latest_device_session_state = "scanning"; },
|
||
(candidate) => { candidate.application_control_session.transport.latest_device_project_bound = false; },
|
||
(candidate) => { candidate.application_control_session.transport.latest_device_init_ready = true; },
|
||
(candidate) => { candidate.application_control_session.verified_control.control_proof_fresh = false; },
|
||
(candidate) => { candidate.connection_supervisor.authority.acquisition_start_allowed = false; },
|
||
(candidate) => { candidate.acquisition.device_id = "another-device"; },
|
||
(candidate) => { candidate.application_control_session.verified_control.intent_id = "stale-intent"; },
|
||
(candidate) => { candidate.application_control_session.verified_control.host_path_epoch = 2; },
|
||
(candidate) => { candidate.application_control_session.verified_control.transport_ref = "stale-transport"; },
|
||
(candidate) => { candidate.application_control_session.verified_control.target_port = 1884; },
|
||
(candidate) => { candidate.connection_lifecycle.ready_to_start = false; },
|
||
]) {
|
||
const stale = structuredClone(state);
|
||
mutate(stale);
|
||
assert.equal(physicalCommandConfirmation.preparedStartTarget(stale), null);
|
||
}
|
||
});
|
||
|
||
test("physical K1 modal checkpoint stays stable on refresh and invalidates on topology, CAS or state changes", () => {
|
||
const state = {
|
||
...supervisedConnectionState(),
|
||
phase: "ready",
|
||
source_mode: "idle",
|
||
selected_device_id: "ble-k1-001",
|
||
device_ref: { device_id: "device-k1-001" },
|
||
device_session: {
|
||
device_session_id: "device-session-001",
|
||
device_id: "device-k1-001",
|
||
connectivity: "connected",
|
||
},
|
||
acquisition: {
|
||
acquisition_id: "acquisition-fence-1",
|
||
device_id: "device-k1-001",
|
||
device_session_id: "device-session-001",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
control_mode: "plugin-commanded",
|
||
project_name: "FENCE01",
|
||
requested_streams: [],
|
||
target_host: "127.0.0.1",
|
||
duration_seconds: null,
|
||
evidence_policy: "required",
|
||
state: "prepared",
|
||
state_revision: 21,
|
||
},
|
||
application_control_session: {
|
||
session_generation: 13,
|
||
state_revision: 34,
|
||
state: "project-ready",
|
||
control_socket_open: true,
|
||
can_start: true,
|
||
verified_control: {
|
||
logical_device_id: "device-k1-001",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
control_session_id: "control-session-001",
|
||
source: "mqtt-device-info",
|
||
intent_id: "intent-bridge-1",
|
||
transport_ref: "ble-k1-001",
|
||
host_path_epoch: 3,
|
||
target_ipv4: "192.168.68.50",
|
||
target_port: 1883,
|
||
connection_mode: "bridge",
|
||
control_proof_revision: 55,
|
||
control_proof_source: "device-status",
|
||
control_proof_fresh: true,
|
||
},
|
||
transport: {
|
||
latest_device_session_state: "ready",
|
||
latest_device_project_bound: true,
|
||
latest_device_init_ready: false,
|
||
},
|
||
},
|
||
};
|
||
const openingTarget = physicalCommandConfirmation.preparedStartTarget(state);
|
||
assert.ok(openingTarget);
|
||
const checkpoint = physicalCommandConfirmation.createPhysicalCommandCheckpoint(
|
||
"start",
|
||
openingTarget,
|
||
);
|
||
|
||
const readOnlyRefresh = structuredClone(state);
|
||
readOnlyRefresh.snapshot_revision = 999;
|
||
// The supervisor revision is an observation counter, not a topology
|
||
// generation: a semantically identical host/MQTT poll increments it.
|
||
// Such a refresh must not destroy an operator confirmation in progress.
|
||
readOnlyRefresh.connection_supervisor.revision += 1;
|
||
readOnlyRefresh.snapshot_runtime_started_at_utc = "2026-08-06T12:01:00Z";
|
||
readOnlyRefresh.connection_supervisor.observed.host_path.observed_at =
|
||
"2026-08-06T12:01:00Z";
|
||
readOnlyRefresh.connection_supervisor.observed.endpoint.observed_at =
|
||
"2026-08-06T12:01:00Z";
|
||
const unchangedTarget = physicalCommandConfirmation.preparedStartTarget(readOnlyRefresh);
|
||
assert.ok(unchangedTarget);
|
||
assert.equal(
|
||
physicalCommandConfirmation.physicalCommandCheckpointMatches(
|
||
checkpoint,
|
||
"start",
|
||
unchangedTarget,
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
physicalCommandConfirmation.physicalCommandCheckpointMatches(
|
||
checkpoint,
|
||
"stop",
|
||
unchangedTarget,
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(Object.isFrozen(checkpoint), true);
|
||
assert.equal(Object.isFrozen(checkpoint.fence), true);
|
||
assert.equal(Object.isFrozen(checkpoint.target), true);
|
||
|
||
const topologyChanged = structuredClone(state);
|
||
topologyChanged.connection_supervisor.revision += 1;
|
||
topologyChanged.connection_supervisor.intent.intent_id = "intent-bridge-2";
|
||
topologyChanged.connection_supervisor.observed.device_network.intent_id = "intent-bridge-2";
|
||
topologyChanged.connection_supervisor.observed.endpoint.intent_id = "intent-bridge-2";
|
||
topologyChanged.connection_supervisor.observed.device_identity.intent_id = "intent-bridge-2";
|
||
topologyChanged.connection_supervisor.lease.intent_id = "intent-bridge-2";
|
||
topologyChanged.connection_lifecycle.active_binding.intent_id = "intent-bridge-2";
|
||
topologyChanged.connection_lifecycle.active_binding.binding_key = "binding-intent-bridge-2-3";
|
||
topologyChanged.connection_lifecycle.active_binding_key = "binding-intent-bridge-2-3";
|
||
topologyChanged.application_control_session.verified_control.intent_id = "intent-bridge-2";
|
||
const topologyTarget = physicalCommandConfirmation.preparedStartTarget(topologyChanged);
|
||
assert.ok(topologyTarget);
|
||
assert.equal(
|
||
physicalCommandConfirmation.physicalCommandCheckpointMatches(
|
||
checkpoint,
|
||
"start",
|
||
topologyTarget,
|
||
),
|
||
false,
|
||
);
|
||
|
||
const casChanged = structuredClone(state);
|
||
casChanged.application_control_session.session_generation += 1;
|
||
casChanged.application_control_session.state_revision += 1;
|
||
const casTarget = physicalCommandConfirmation.preparedStartTarget(casChanged);
|
||
assert.ok(casTarget);
|
||
assert.equal(
|
||
physicalCommandConfirmation.physicalCommandCheckpointMatches(
|
||
checkpoint,
|
||
"start",
|
||
casTarget,
|
||
),
|
||
false,
|
||
);
|
||
|
||
const runtimeStateChanged = structuredClone(state);
|
||
runtimeStateChanged.phase = "degraded";
|
||
const runtimeTarget = physicalCommandConfirmation.preparedStartTarget(runtimeStateChanged);
|
||
assert.ok(runtimeTarget);
|
||
assert.equal(
|
||
physicalCommandConfirmation.physicalCommandCheckpointMatches(
|
||
checkpoint,
|
||
"start",
|
||
runtimeTarget,
|
||
),
|
||
false,
|
||
);
|
||
|
||
assert.equal(checkpoint.target.projectName, "FENCE01");
|
||
const changedDisplay = structuredClone(openingTarget);
|
||
changedDisplay.projectName = "FENCE02";
|
||
assert.equal(
|
||
physicalCommandConfirmation.physicalCommandCheckpointMatches(
|
||
checkpoint,
|
||
"start",
|
||
changedDisplay,
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(checkpoint.target.projectName, "FENCE01");
|
||
});
|
||
|
||
test("K1 STOP target is bound to the active acquisition instead of START constants", () => {
|
||
const state = {
|
||
compatibility: {
|
||
permitted_mode: "active-control",
|
||
vendor_writes_enabled: true,
|
||
},
|
||
acquisition: {
|
||
acquisition_id: "acquisition-active-77",
|
||
device_id: "device-k1-active",
|
||
device_session_id: "device-session-active",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
control_mode: "plugin-commanded",
|
||
project_name: "ROUTE77",
|
||
requested_streams: [],
|
||
target_host: "127.0.0.1",
|
||
duration_seconds: null,
|
||
evidence_policy: "required",
|
||
state: "acquiring",
|
||
state_revision: 12,
|
||
},
|
||
application_control_session: {
|
||
state: "scanning",
|
||
verified_control: {
|
||
logical_device_id: "device-k1-active",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
connection_mode: "quick-connect",
|
||
target_ipv4: "192.168.56.1",
|
||
target_port: 1883,
|
||
},
|
||
transport: {
|
||
latest_device_session_state: "scanning",
|
||
latest_device_project_bound: true,
|
||
latest_device_init_ready: true,
|
||
},
|
||
},
|
||
};
|
||
|
||
const target = physicalCommandConfirmation.activeStopTarget(state);
|
||
assert.ok(target);
|
||
const { fence, ...displayTarget } = target;
|
||
assert.deepEqual(displayTarget, {
|
||
deviceId: "device-k1-active",
|
||
connection: "quick-connect · 192.168.56.1:1883",
|
||
projectName: "ROUTE77",
|
||
acquisitionId: "acquisition-active-77",
|
||
deviceState: "SCANNING · проект привязан · инициализация завершена",
|
||
});
|
||
assert.equal(fence.kind, "stop");
|
||
assert.equal(fence.acquisitionStateRevision, 12);
|
||
assert.equal(
|
||
physicalCommandConfirmation.activeStopTarget({
|
||
...state,
|
||
acquisition: { ...state.acquisition, control_mode: "operator-manual" },
|
||
}),
|
||
null,
|
||
);
|
||
});
|
||
|
||
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 = {
|
||
...supervisedConnectionState({ dataAuthoritative: true }),
|
||
...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");
|
||
const staleDataEpoch = structuredClone(acquiring);
|
||
staleDataEpoch.connection_supervisor.observed.data_plane.host_path_epoch = 2;
|
||
assert.equal(lifecycle.isConfirmedLiveState(staleDataEpoch), false);
|
||
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("physical-command guidance projects backend policy without exposing reason codes", () => {
|
||
const denied = {
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
allowed_actions: ["stop-local-receiver"],
|
||
recommended_action: "stop-local-receiver",
|
||
actions: {
|
||
"stop-acquisition": {
|
||
allowed: false,
|
||
reason_codes: ["physical-command-reconciliation-required"],
|
||
target_source: "connection-supervisor",
|
||
required_transport_ref: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
facts: { retained_context_is_presence: false },
|
||
},
|
||
};
|
||
const guidance = presentation.connectionPolicyOperatorGuidance(
|
||
denied,
|
||
"stop-acquisition",
|
||
);
|
||
assert.deepEqual(guidance, {
|
||
reason: "Результат предыдущей физической команды K1 не подтверждён.",
|
||
nextAction: "Завершите локальный приём; физическое состояние K1 проверьте вручную.",
|
||
});
|
||
assert.doesNotMatch(`${guidance.reason} ${guidance.nextAction}`, /physical-command|stop-local/);
|
||
|
||
const allowed = structuredClone(denied);
|
||
allowed.connection_policy.allowed_actions.push("stop-acquisition");
|
||
allowed.connection_policy.actions["stop-acquisition"].allowed = true;
|
||
allowed.connection_policy.actions["stop-acquisition"].reason_codes = [];
|
||
assert.equal(
|
||
presentation.connectionPolicyOperatorGuidance(allowed, "stop-acquisition"),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test("restart-recovery guidance offers an explicit read-only recovery", () => {
|
||
const deniedFresh = {
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
allowed_actions: ["observe-configured-device-network"],
|
||
recommended_action: "observe-configured-device-network",
|
||
actions: {
|
||
"observe-fresh-device-network": {
|
||
allowed: false,
|
||
reason_codes: ["reconciliation-target-not-observed"],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: "exact-durable-k1",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
"observe-configured-device-network": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: "exact-durable-k1",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
facts: { retained_context_is_presence: false },
|
||
},
|
||
};
|
||
|
||
const durableGuidance = presentation.connectionPolicyOperatorGuidance(
|
||
deniedFresh,
|
||
"observe-fresh-device-network",
|
||
);
|
||
assert.deepEqual(durableGuidance, {
|
||
reason: "В свежем Bluetooth-поиске не найден K1, связанный с незавершённой записью.",
|
||
nextAction: "Нажмите «Переподключиться», чтобы проверить сохранённое подключение K1.",
|
||
});
|
||
assert.doesNotMatch(
|
||
`${durableGuidance.reason} ${durableGuidance.nextAction}`,
|
||
/observe-configured|durable-configured/,
|
||
);
|
||
|
||
deniedFresh.connection_policy.recommended_action = "observe-current-device-network";
|
||
assert.equal(
|
||
presentation.connectionPolicyOperatorGuidance(
|
||
deniedFresh,
|
||
"observe-fresh-device-network",
|
||
).nextAction,
|
||
"Нажмите «Переподключиться», чтобы проверить связь с тем же K1.",
|
||
);
|
||
});
|
||
|
||
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" },
|
||
}), "configuring");
|
||
assert.equal(lifecycle.normalizeRuntimePhase({
|
||
...supervisedConnectionState({
|
||
leaseState: "configured-unverified",
|
||
controlAllowed: false,
|
||
}),
|
||
phase: "connected",
|
||
}), "connected");
|
||
});
|
||
|
||
test("a released acquisition failure stays exact in the acquisition panel without poisoning global runtime", () => {
|
||
const releasedFailure = {
|
||
phase: "error",
|
||
source_mode: "idle",
|
||
acquisition: {
|
||
state: "failed",
|
||
cleanup_pending: false,
|
||
},
|
||
application_control_session: {
|
||
state: "idle",
|
||
failure: {
|
||
network_change_admissible: true,
|
||
message: "control connection was lost after acknowledged stop",
|
||
},
|
||
},
|
||
};
|
||
|
||
assert.equal(lifecycle.isReleasedTerminalAcquisitionFailure(releasedFailure), true);
|
||
assert.equal(lifecycle.normalizeRuntimePhase(releasedFailure), "idle");
|
||
assert.equal(lifecycle.shouldSurfaceRuntimeActionError("live", releasedFailure), false);
|
||
assert.equal(lifecycle.shouldSurfaceRuntimeActionError("control", releasedFailure), false);
|
||
assert.equal(lifecycle.shouldSurfaceRuntimeActionError("scan", releasedFailure), true);
|
||
assert.equal(
|
||
lifecycle.authoritativeStateSupersedesRuntimeError(
|
||
{ action: "stop", runtimeId: "runtime-a", leaseGeneration: 4 },
|
||
releasedFailure,
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
lifecycle.authoritativeStateSupersedesRuntimeError(
|
||
{ action: "scan", runtimeId: "runtime-a", leaseGeneration: 4 },
|
||
releasedFailure,
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
lifecycle.isReleasedTerminalAcquisitionFailure({
|
||
...releasedFailure,
|
||
acquisition: { state: "failed", cleanup_pending: true },
|
||
}),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
lifecycle.isReleasedTerminalAcquisitionFailure({
|
||
...releasedFailure,
|
||
application_control_session: {
|
||
state: "failed",
|
||
failure: { network_change_admissible: false },
|
||
},
|
||
}),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("poll and WebSocket state acceptance clear stale runtime banners after local release", async () => {
|
||
const runtimeSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
|
||
assert.match(runtimeSource, /authoritativeStateSupersedesRuntimeError\(\s*errorCorrelation\.current,\s*acceptedState/);
|
||
assert.match(runtimeSource, /openEventSocket\(acceptState/);
|
||
});
|
||
|
||
test("observed SCANNING recovery is a successful STOP-only connection outcome", async () => {
|
||
const recoveredScanning = {
|
||
...supervisedConnectionState({ mode: "bridge" }),
|
||
connection_mode: "bridge",
|
||
active_connection_mode: "bridge",
|
||
application_control_session: {
|
||
state: "scanning",
|
||
can_stop: true,
|
||
physical_command: {
|
||
requires_reconciliation: false,
|
||
resolved_active_recovery_required: true,
|
||
observed_session_state: "scanning",
|
||
},
|
||
},
|
||
};
|
||
|
||
assert.equal(lifecycle.isRecoveredPhysicalScanning(recoveredScanning, "bridge"), true);
|
||
assert.equal(
|
||
lifecycle.isRecoveredPhysicalScanning(recoveredScanning, "quick-connect"),
|
||
false,
|
||
);
|
||
const replayDuringPhysicalRecovery = {
|
||
...recoveredScanning,
|
||
source_mode: "replay",
|
||
compatibility: {
|
||
vendor_writes_enabled: true,
|
||
permitted_mode: "active-control",
|
||
},
|
||
acquisition: {
|
||
acquisition_id: "terminal-live-acquisition",
|
||
device_id: "original-k1",
|
||
device_session_id: "fresh-verified-session",
|
||
project_name: null,
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
control_mode: "plugin-commanded",
|
||
requested_streams: [],
|
||
target_host: "192.168.68.52",
|
||
evidence_policy: "disabled",
|
||
state: "failed",
|
||
state_revision: 9,
|
||
cleanup_pending: false,
|
||
result: {
|
||
recovery_only: true,
|
||
device_state: "scanning",
|
||
automatic_replay_allowed: false,
|
||
},
|
||
},
|
||
};
|
||
assert.equal(
|
||
lifecycle.requiresCanonicalStopAfterTerminalLocalFailure(replayDuringPhysicalRecovery),
|
||
true,
|
||
);
|
||
const recoveredStopTarget = physicalCommandConfirmation.activeStopTarget(
|
||
replayDuringPhysicalRecovery,
|
||
);
|
||
assert.ok(recoveredStopTarget);
|
||
assert.equal(recoveredStopTarget.acquisitionId, "terminal-live-acquisition");
|
||
assert.equal(recoveredStopTarget.deviceId, "original-k1");
|
||
assert.equal(recoveredStopTarget.fence.kind, "stop");
|
||
|
||
const runtimeSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
const provisioningSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/components/K1ProvisioningPipeline.tsx",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
const acquisitionSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/components/K1AcquisitionPipeline.tsx",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
assert.match(runtimeSource, /requireExactReadOnlyVerificationOutcome/);
|
||
assert.match(runtimeSource, /isRecoveredPhysicalScanning\(failedState, requestedConnectionMode\)/);
|
||
assert.match(provisioningSource, /isPhysicalStopRecoverySettling\(state\)/);
|
||
assert.match(
|
||
provisioningSource,
|
||
/const canScan = backendScanAllowed && !physicalStopRecoverySettling/,
|
||
);
|
||
assert.doesNotMatch(
|
||
provisioningSource,
|
||
/K1 продолжает сканирование|Завершаем остановку K1/,
|
||
);
|
||
assert.match(acquisitionSource, /recoveredPhysicalStop\s*\? "Требуется остановка"/);
|
||
assert.match(acquisitionSource, /terminalPhysicalStopObserved\s*\? "warning"/);
|
||
assert.match(acquisitionSource, /recoveredPhysicalStop \? "Сканирование продолжается"/);
|
||
assert.match(acquisitionSource, /Нажмите «Остановить сканирование» ниже или в пространственной сцене/);
|
||
assert.match(
|
||
acquisitionSource,
|
||
/canIssueCanonicalStop\(state, physicalStopIntentSpent\)/,
|
||
);
|
||
assert.match(
|
||
acquisitionSource,
|
||
/physicalStopInFlight\s*\? "Остановка устройства…"/,
|
||
);
|
||
assert.match(
|
||
acquisitionSource,
|
||
/: physicalStopPresented \? recoveredPhysicalStop \? "Остановить сканирование" : "Остановить устройство и запись"/,
|
||
);
|
||
assert.ok(
|
||
acquisitionSource.indexOf("terminalPhysicalStopObserved ? (")
|
||
< acquisitionSource.indexOf("<div className=\"scan-configuration-grid\">"),
|
||
"recovered SCANNING must render the STOP-only branch before project/START controls",
|
||
);
|
||
});
|
||
|
||
test("K1 provisioning mutations require the operator's fresh BLE candidate", () => {
|
||
const backendLease = {
|
||
selected_device_id: "stale-backend-lease-device",
|
||
connection_mode: "bridge",
|
||
devices: [
|
||
{
|
||
device_id: "fresh-scan-candidate",
|
||
name: "K1 candidate",
|
||
connectable: true,
|
||
},
|
||
],
|
||
};
|
||
|
||
assert.equal(
|
||
lifecycle.provisioningCandidateById(backendLease.devices, ""),
|
||
null,
|
||
);
|
||
assert.equal(
|
||
lifecycle.provisioningCandidateById(
|
||
backendLease.devices,
|
||
backendLease.selected_device_id,
|
||
),
|
||
null,
|
||
);
|
||
assert.equal(lifecycle.canSubmitProvisioningMutation({
|
||
devices: backendLease.devices,
|
||
selectedDeviceId: "expired-scan-candidate",
|
||
credentialsReady: true,
|
||
isBusy: false,
|
||
}), false);
|
||
assert.equal(lifecycle.canSubmitProvisioningMutation({
|
||
devices: backendLease.devices,
|
||
selectedDeviceId: "fresh-scan-candidate",
|
||
credentialsReady: true,
|
||
isBusy: false,
|
||
}), true);
|
||
});
|
||
|
||
test("K1 connection status is green only for a reachable matching lease", () => {
|
||
const legacyReachableLease = {
|
||
k1_ip: "192.168.68.50",
|
||
connection_mode: "bridge",
|
||
connection_verification: {
|
||
lease_state: "reachable",
|
||
network_reachability: "reachable",
|
||
},
|
||
};
|
||
const reachableLease = supervisedConnectionState();
|
||
|
||
assert.equal(lifecycle.isReachableConnectionLease(legacyReachableLease, "bridge"), false);
|
||
assert.equal(lifecycle.isReachableConnectionLease(reachableLease, "bridge"), true);
|
||
assert.equal(lifecycle.isReachableConnectionLease(reachableLease, "quick-connect"), false);
|
||
assert.equal(lifecycle.isReachableConnectionLease({
|
||
k1_ip: "192.168.68.50",
|
||
connection_mode: "bridge",
|
||
}, "bridge"), false);
|
||
assert.equal(lifecycle.isReachableConnectionLease({
|
||
k1_ip: "192.168.56.1",
|
||
connection_mode: "quick-connect",
|
||
connection_verification: {
|
||
status: "control-transport-lost",
|
||
lease_state: "disconnected",
|
||
network_reachability: "unreachable",
|
||
},
|
||
}, "quick-connect"), false);
|
||
|
||
for (const mutate of [
|
||
(state) => { state.connection_supervisor.observed.device_network.state = "unconfigured"; },
|
||
(state) => { state.connection_supervisor.observed.device_network.intent_id = "stale-intent"; },
|
||
(state) => { state.connection_supervisor.observed.device_network.transport_ref = null; },
|
||
(state) => {
|
||
state.connection_supervisor.observed.device_network.target = {
|
||
...state.connection_supervisor.observed.device_network.target,
|
||
ipv4: "192.168.68.99",
|
||
};
|
||
},
|
||
(state) => { state.connection_supervisor.observed.endpoint.intent_id = "stale-intent"; },
|
||
(state) => { state.connection_supervisor.observed.endpoint.host_path_epoch = 2; },
|
||
(state) => { state.connection_supervisor.observed.host_path.route_class = "default"; },
|
||
(state) => { state.connection_supervisor.observed.device_identity.intent_id = "stale-intent"; },
|
||
(state) => { state.connection_supervisor.observed.device_identity.host_path_epoch = 2; },
|
||
(state) => { state.connection_supervisor.observed.control_plane.host_path_epoch = 2; },
|
||
(state) => { state.connection_supervisor.observed.control_plane.session_id = null; },
|
||
(state) => {
|
||
state.connection_supervisor.observed.endpoint.target = {
|
||
...state.connection_supervisor.observed.endpoint.target,
|
||
ipv4: "192.168.68.99",
|
||
};
|
||
},
|
||
]) {
|
||
const stale = structuredClone(reachableLease);
|
||
mutate(stale);
|
||
assert.equal(lifecycle.isReachableConnectionLease(stale, "bridge"), false);
|
||
}
|
||
});
|
||
|
||
test("frontend connection authority fails closed without the backend lifecycle projection", () => {
|
||
const reachable = supervisedConnectionState();
|
||
assert.equal(lifecycle.isReachableConnectionLease(reachable, "bridge"), true);
|
||
|
||
const omitted = structuredClone(reachable);
|
||
delete omitted.connection_lifecycle;
|
||
assert.equal(lifecycle.isReachableConnectionLease(omitted, "bridge"), false);
|
||
assert.equal(
|
||
lifecycle.currentAppliedConnectionTopology(omitted, "bridge")?.status,
|
||
"configured-unverified",
|
||
);
|
||
|
||
const drifted = structuredClone(reachable);
|
||
drifted.connection_lifecycle.active_mode = "quick-connect";
|
||
assert.equal(lifecycle.isReachableConnectionLease(drifted, "bridge"), false);
|
||
});
|
||
|
||
test("backend topology distinguishes reachable authority from last-known address", () => {
|
||
const target = { ipv4: "192.168.56.1", port: 1883 };
|
||
const lastKnown = supervisedConnectionState({
|
||
mode: "quick-connect",
|
||
target,
|
||
leaseState: "lost",
|
||
controlAllowed: false,
|
||
deviceNetworkState: "unconfigured",
|
||
lastKnown: {
|
||
connection_mode: "quick-connect",
|
||
target,
|
||
logical_device_id: "device-k1-001",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
verified_at: "2026-08-06T11:59:00Z",
|
||
},
|
||
});
|
||
const configured = supervisedConnectionState({
|
||
mode: "quick-connect",
|
||
target,
|
||
leaseState: "configured-unverified",
|
||
controlAllowed: false,
|
||
});
|
||
const active = supervisedConnectionState({ mode: "quick-connect", target });
|
||
|
||
assert.deepEqual(lifecycle.backendConnectionTopology(lastKnown), {
|
||
connectionMode: "quick-connect",
|
||
status: "configured-offline",
|
||
source: "last-known",
|
||
endpoint: "192.168.56.1",
|
||
});
|
||
assert.deepEqual(lifecycle.backendConnectionTopology(configured), {
|
||
connectionMode: "quick-connect",
|
||
status: "configured-unverified",
|
||
source: "applied",
|
||
endpoint: "192.168.56.1",
|
||
});
|
||
assert.deepEqual(lifecycle.backendConnectionTopology(active), {
|
||
connectionMode: "quick-connect",
|
||
status: "active",
|
||
source: "applied",
|
||
endpoint: "192.168.56.1",
|
||
});
|
||
assert.equal(lifecycle.activeConnectionEndpointLabel(lastKnown), null);
|
||
assert.equal(
|
||
lifecycle.activeConnectionEndpointLabel(active),
|
||
"192.168.56.1",
|
||
);
|
||
assert.equal(lifecycle.backendConnectionTopology({}), null);
|
||
});
|
||
|
||
test("current BLE topology remains visible offline and supersedes durable history", () => {
|
||
const currentOffline = supervisedConnectionState({
|
||
mode: "bridge",
|
||
leaseState: "lost",
|
||
controlAllowed: false,
|
||
hostAvailable: false,
|
||
endpointState: "unreachable",
|
||
});
|
||
currentOffline.semantic_topology_store = {
|
||
status: "available",
|
||
configured_offline_evidence: true,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: {
|
||
schema_version: "missioncore.xgrids-k1-semantic-topology/v1",
|
||
revision: 2,
|
||
transport_ref: "ble-k1-old",
|
||
connection_mode: "quick-connect",
|
||
ipv4: "192.168.56.1",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
firmware_version: "3.0.2",
|
||
source: "ble-read-only-status",
|
||
observed_at_utc: "2026-08-06T11:00:00Z",
|
||
},
|
||
};
|
||
|
||
assert.deepEqual(lifecycle.backendConnectionTopology(currentOffline), {
|
||
connectionMode: "bridge",
|
||
status: "configured-offline",
|
||
source: "applied",
|
||
endpoint: "192.168.68.50",
|
||
});
|
||
assert.equal(lifecycle.backendConnectionTopology(currentOffline, "quick-connect"), null);
|
||
assert.equal(lifecycle.isReachableConnectionLease(currentOffline, "bridge"), false);
|
||
assert.equal(lifecycle.isConfiguredConnectionLease(currentOffline, "bridge"), true);
|
||
assert.equal(lifecycle.hasControlAuthority(currentOffline), false);
|
||
assert.equal(
|
||
lifecycle.normalizeRuntimePhase({ ...currentOffline, phase: "connected" }),
|
||
"configuring",
|
||
);
|
||
assert.equal(lifecycle.canonicalDeviceConnectivity(currentOffline), "offline");
|
||
});
|
||
|
||
test("durable semantic topology is configured-offline evidence, never authority", () => {
|
||
const durable = {
|
||
semantic_topology_store: {
|
||
status: "available",
|
||
configured_offline_evidence: true,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: {
|
||
schema_version: "missioncore.xgrids-k1-semantic-topology/v1",
|
||
revision: 4,
|
||
transport_ref: "ble-k1-001",
|
||
connection_mode: "quick-connect",
|
||
ipv4: "192.168.56.1",
|
||
compatibility_profile_id: "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2",
|
||
firmware_version: "3.0.2",
|
||
source: "ble-post-write-status",
|
||
observed_at_utc: "2026-08-06T12:00:00Z",
|
||
},
|
||
},
|
||
};
|
||
assert.deepEqual(lifecycle.backendConnectionTopology(durable), {
|
||
connectionMode: "quick-connect",
|
||
status: "configured-offline",
|
||
source: "durable",
|
||
endpoint: "192.168.56.1",
|
||
});
|
||
assert.equal(lifecycle.isConfiguredConnectionLease(durable, "quick-connect"), true);
|
||
assert.equal(lifecycle.isReachableConnectionLease(durable, "quick-connect"), false);
|
||
assert.equal(lifecycle.hasControlAuthority(durable), false);
|
||
assert.equal(lifecycle.normalizeRuntimePhase({ ...durable, phase: "connected" }), "configuring");
|
||
assert.equal(lifecycle.canonicalDeviceConnectivity(durable), "offline");
|
||
|
||
const hostProbed = structuredClone(durable);
|
||
hostProbed.configured_endpoint_probe = {
|
||
schema_version: "missioncore.xgrids-k1-configured-endpoint-probe/v1",
|
||
status: "reachable",
|
||
target_source: "durable-semantic-topology",
|
||
connection_mode: "quick-connect",
|
||
endpoint: "192.168.56.1",
|
||
transport_ref: "ble-k1-001",
|
||
intent_id: null,
|
||
semantic_revision: 4,
|
||
host_route_available: true,
|
||
host_route_class: "direct",
|
||
tcp_reachable: true,
|
||
identity_validation: "not-performed",
|
||
control_authority_granted: false,
|
||
ble_operation_performed: false,
|
||
network_mutation_performed: false,
|
||
automatic_retry: false,
|
||
observed_at: "2026-08-08T12:30:19Z",
|
||
reason_code: null,
|
||
};
|
||
assert.deepEqual(lifecycle.backendConnectionTopology(hostProbed), {
|
||
connectionMode: "quick-connect",
|
||
status: "configured-unverified",
|
||
source: "durable",
|
||
endpoint: "192.168.56.1",
|
||
});
|
||
assert.equal(lifecycle.isReachableConnectionLease(hostProbed, "quick-connect"), false);
|
||
assert.equal(lifecycle.hasControlAuthority(hostProbed), false);
|
||
|
||
for (const status of ["empty", "corrupt"]) {
|
||
const unavailable = structuredClone(durable);
|
||
unavailable.semantic_topology_store.status = status;
|
||
unavailable.semantic_topology_store.record = null;
|
||
unavailable.semantic_topology_store.configured_offline_evidence = false;
|
||
assert.equal(lifecycle.backendConnectionTopology(unavailable), null);
|
||
}
|
||
});
|
||
|
||
test("unresolved or corrupt durable mutation state remains a provisioning barrier", () => {
|
||
assert.equal(lifecycle.hasUnresolvedNetworkMutation({}), false);
|
||
assert.equal(lifecycle.hasUnresolvedNetworkMutation({ network_write_reconciliation: {} }), true);
|
||
assert.equal(lifecycle.hasUnresolvedNetworkMutation({
|
||
network_mutation_ledger: { status: "unresolved", mutation_allowed: false },
|
||
}), true);
|
||
assert.equal(lifecycle.hasUnresolvedNetworkMutation({
|
||
network_mutation_ledger: { status: "corrupt", mutation_allowed: false },
|
||
}), true);
|
||
assert.equal(lifecycle.hasUnresolvedNetworkMutation({
|
||
network_mutation_ledger: { status: "resolved", mutation_allowed: true },
|
||
}), false);
|
||
});
|
||
|
||
test("retained BLE context is never presence but can carry one backend-authorized recovery", () => {
|
||
const activeQuick = {
|
||
current_device_recovery: {
|
||
transport_ref: "exact-session-handle",
|
||
connection_mode: "quick-connect",
|
||
handle_available: true,
|
||
handle_retained: true,
|
||
advertised_now: false,
|
||
gatt_validated_recently: true,
|
||
},
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
allowed_actions: ["recover-current-device-network"],
|
||
actions: {
|
||
"recover-current-device-network": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "retained-current-process",
|
||
required_transport_ref: "exact-session-handle",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
facts: {
|
||
retained_context_is_presence: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
assert.equal(
|
||
lifecycle.currentDeviceTransportRef(activeQuick),
|
||
"exact-session-handle",
|
||
);
|
||
assert.deepEqual(lifecycle.retainedBleRecoveryTarget(activeQuick), {
|
||
transportRef: "exact-session-handle",
|
||
connectionMode: "quick-connect",
|
||
gattValidatedRecently: true,
|
||
});
|
||
assert.equal(
|
||
lifecycle.provisioningCandidateById([], "exact-session-handle"),
|
||
null,
|
||
);
|
||
assert.equal(
|
||
lifecycle.canSubmitProvisioningMutation({
|
||
devices: [],
|
||
selectedDeviceId: "exact-session-handle",
|
||
credentialsReady: true,
|
||
isBusy: false,
|
||
}),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
lifecycle.connectionPolicyAllows(
|
||
activeQuick,
|
||
"recover-current-device-network",
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
lifecycle.connectionPolicyDecision(
|
||
activeQuick,
|
||
"recover-current-device-network",
|
||
).requires_live_gatt_validation,
|
||
true,
|
||
);
|
||
|
||
const freshlyObserved = {
|
||
device_id: "exact-session-handle",
|
||
name: "Lixel K1",
|
||
connectable: true,
|
||
};
|
||
assert.equal(
|
||
lifecycle.provisioningCandidateById(
|
||
[freshlyObserved],
|
||
"exact-session-handle",
|
||
),
|
||
freshlyObserved,
|
||
);
|
||
assert.equal(
|
||
lifecycle.canSubmitProvisioningMutation({
|
||
devices: [freshlyObserved],
|
||
selectedDeviceId: "exact-session-handle",
|
||
credentialsReady: true,
|
||
isBusy: false,
|
||
}),
|
||
true,
|
||
);
|
||
assert.equal(lifecycle.retainedBleRecoveryTarget({
|
||
current_device_recovery: {
|
||
...activeQuick.current_device_recovery,
|
||
advertised_now: true,
|
||
},
|
||
}), null);
|
||
});
|
||
|
||
test("browser refresh never adopts an existing backend K1 session as selection", () => {
|
||
const backendSession = {
|
||
selected_device_id: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
device_session: {
|
||
device_session_id: "device-session-001",
|
||
device_id: "logical-k1-001",
|
||
},
|
||
current_device_recovery: {
|
||
transport_ref: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
handle_retained: true,
|
||
},
|
||
};
|
||
|
||
assert.equal(
|
||
lifecycle.locallyInitiatedBleSessionTarget(backendSession, null, "bridge"),
|
||
null,
|
||
);
|
||
assert.equal(
|
||
lifecycle.locallyInitiatedBleSessionTarget(
|
||
backendSession,
|
||
"another-device",
|
||
"bridge",
|
||
),
|
||
null,
|
||
);
|
||
assert.equal(
|
||
lifecycle.locallyInitiatedBleSessionTarget(
|
||
backendSession,
|
||
"ble-k1-001",
|
||
"quick-connect",
|
||
),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test("a pending local connect cannot bind the old matching backend session", () => {
|
||
const oldBackendSession = {
|
||
selected_device_id: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
device_session: {
|
||
device_session_id: "device-session-A",
|
||
device_id: "logical-k1-001",
|
||
},
|
||
current_device_recovery: {
|
||
transport_ref: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
handle_retained: true,
|
||
},
|
||
};
|
||
|
||
const oldTarget = lifecycle.bleSessionTargetForTransport(
|
||
oldBackendSession,
|
||
"ble-k1-001",
|
||
"bridge",
|
||
);
|
||
assert.equal(oldTarget?.key, "device-session-A:bridge:ble-k1-001");
|
||
assert.equal(
|
||
lifecycle.locallyInitiatedBleSessionTarget(
|
||
oldBackendSession,
|
||
null,
|
||
"bridge",
|
||
),
|
||
null,
|
||
"pending is not a successful local intent and grants no bind authority",
|
||
);
|
||
assert.equal(
|
||
lifecycle.acceptedBleSessionKeyAfterConnect(
|
||
oldBackendSession,
|
||
"ble-k1-001",
|
||
"bridge",
|
||
oldTarget.key,
|
||
),
|
||
null,
|
||
"the session that predated the click is not an accepted result",
|
||
);
|
||
});
|
||
|
||
test("a successful local connect binds only the new matching backend session", () => {
|
||
const backendSession = {
|
||
selected_device_id: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
device_session: {
|
||
device_session_id: "device-session-B",
|
||
device_id: "logical-k1-001",
|
||
},
|
||
current_device_recovery: {
|
||
transport_ref: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
handle_retained: true,
|
||
},
|
||
};
|
||
|
||
const acceptedSessionKey = lifecycle.acceptedBleSessionKeyAfterConnect(
|
||
backendSession,
|
||
"ble-k1-001",
|
||
"bridge",
|
||
"device-session-A:bridge:ble-k1-001",
|
||
);
|
||
assert.equal(acceptedSessionKey, "device-session-B:bridge:ble-k1-001");
|
||
|
||
assert.deepEqual(
|
||
lifecycle.locallyInitiatedBleSessionTarget(
|
||
backendSession,
|
||
"ble-k1-001",
|
||
"bridge",
|
||
{ requiredSessionKey: acceptedSessionKey },
|
||
),
|
||
{
|
||
transportRef: "ble-k1-001",
|
||
connectionMode: "bridge",
|
||
deviceSessionId: "device-session-B",
|
||
key: "device-session-B:bridge:ble-k1-001",
|
||
},
|
||
);
|
||
assert.equal(
|
||
lifecycle.locallyInitiatedBleSessionTarget(
|
||
{
|
||
...backendSession,
|
||
device_session: {
|
||
...backendSession.device_session,
|
||
device_session_id: "device-session-C",
|
||
},
|
||
},
|
||
"ble-k1-001",
|
||
"bridge",
|
||
{ requiredSessionKey: acceptedSessionKey },
|
||
),
|
||
null,
|
||
"a later session cannot silently replace the accepted session B",
|
||
);
|
||
assert.equal(
|
||
lifecycle.locallyInitiatedBleSessionTarget(
|
||
{ ...backendSession, device_session: null },
|
||
"ble-k1-001",
|
||
"bridge",
|
||
{ requiredSessionKey: acceptedSessionKey },
|
||
),
|
||
null,
|
||
);
|
||
assert.equal(
|
||
lifecycle.acceptedBleSessionKeyAfterConnect(
|
||
{ ...backendSession, device_session: null },
|
||
"ble-k1-001",
|
||
"bridge",
|
||
"device-session-A:bridge:ble-k1-001",
|
||
),
|
||
null,
|
||
"success without an exact returned session key is a clean reset",
|
||
);
|
||
assert.equal(
|
||
lifecycle.canAdmitProvisioningConnection({
|
||
policyAllowed: true,
|
||
targetSource: "fresh-scan",
|
||
hasSuccessfulLocalConnect: false,
|
||
localPrerequisitesReady: true,
|
||
}),
|
||
true,
|
||
"after the reset a fresh explicit scan selection can connect",
|
||
);
|
||
});
|
||
|
||
test("an arbitrary BLE result set never selects or connects a device", () => {
|
||
const devices = Array.from({ length: 20 }, (_, index) => ({
|
||
device_id: `ble-device-${String(index + 1).padStart(2, "0")}`,
|
||
name: `BLE device ${index + 1}`,
|
||
connectable: true,
|
||
}));
|
||
|
||
assert.equal(lifecycle.provisioningCandidateById(devices, ""), null);
|
||
assert.equal(
|
||
lifecycle.canSubmitProvisioningMutation({
|
||
devices,
|
||
selectedDeviceId: "",
|
||
credentialsReady: true,
|
||
isBusy: false,
|
||
}),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("connection policy remains authoritative when historical operations are present", () => {
|
||
const state = {
|
||
operations: [{
|
||
action: "network.provision",
|
||
status: "running",
|
||
operation_id: "historical-operation",
|
||
}],
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["provision-fresh-device"],
|
||
actions: {
|
||
"provision-fresh-device": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: null,
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
},
|
||
};
|
||
|
||
assert.equal(
|
||
lifecycle.connectionPolicyAllows(state, "provision-fresh-device"),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
lifecycle.canAdmitProvisioningConnection({
|
||
policyAllowed: true,
|
||
targetSource: "fresh-scan",
|
||
hasSuccessfulLocalConnect: false,
|
||
localPrerequisitesReady: true,
|
||
}),
|
||
true,
|
||
);
|
||
});
|
||
|
||
test("legacy backend recovery evidence keeps exact UUID and mode outside UI admission", () => {
|
||
const decision = (allowed, target_source, required_transport_ref, required_connection_mode) => ({
|
||
allowed,
|
||
reason_codes: allowed ? [] : ["not-selected"],
|
||
target_source,
|
||
required_transport_ref,
|
||
required_connection_mode,
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
});
|
||
const state = {
|
||
ble_discovery_generation: 7,
|
||
devices: [{
|
||
device_id: "fresh-policy-k1",
|
||
name: "Lixel K1",
|
||
connectable: true,
|
||
}],
|
||
network_mutation_ledger: {
|
||
status: "unresolved",
|
||
mutation_allowed: false,
|
||
},
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
allowed_actions: [
|
||
"observe-fresh-device-network",
|
||
"observe-current-device-network",
|
||
"observe-configured-device-network",
|
||
],
|
||
actions: {
|
||
"observe-fresh-device-network": decision(
|
||
true,
|
||
"fresh-scan",
|
||
"fresh-policy-k1",
|
||
"direct-connect",
|
||
),
|
||
"observe-current-device-network": decision(
|
||
true,
|
||
"retained-current-process",
|
||
"retained-policy-k1",
|
||
"quick-connect",
|
||
),
|
||
"observe-configured-device-network": decision(
|
||
true,
|
||
"durable-configured-state",
|
||
"durable-policy-k1",
|
||
"bridge",
|
||
),
|
||
},
|
||
facts: { retained_context_is_presence: false },
|
||
},
|
||
};
|
||
|
||
assert.deepEqual(
|
||
lifecycle.readOnlyConnectionObservationTarget(
|
||
state,
|
||
"different-browser-selection",
|
||
"bridge",
|
||
),
|
||
{
|
||
action: "observe-fresh-device-network",
|
||
deviceId: "fresh-policy-k1",
|
||
connectionMode: "direct-connect",
|
||
source: "fresh-scan",
|
||
serverBound: true,
|
||
expectedDiscoveryGeneration: 7,
|
||
},
|
||
);
|
||
|
||
state.devices = [];
|
||
state.connection_policy.allowed_actions = [
|
||
"observe-current-device-network",
|
||
"observe-configured-device-network",
|
||
];
|
||
state.connection_policy.actions["observe-fresh-device-network"] = decision(
|
||
false,
|
||
"fresh-scan",
|
||
"fresh-policy-k1",
|
||
"direct-connect",
|
||
);
|
||
assert.deepEqual(
|
||
lifecycle.readOnlyConnectionObservationTarget(
|
||
state,
|
||
"stale-browser-selection",
|
||
"direct-connect",
|
||
),
|
||
{
|
||
action: "observe-current-device-network",
|
||
deviceId: "retained-policy-k1",
|
||
connectionMode: "quick-connect",
|
||
source: "retained-current-process",
|
||
serverBound: true,
|
||
expectedDiscoveryGeneration: null,
|
||
},
|
||
);
|
||
assert.deepEqual(
|
||
lifecycle.serverBoundAppliedNetworkObservationTarget(state, "quick-connect"),
|
||
{
|
||
action: "observe-current-device-network",
|
||
deviceId: "retained-policy-k1",
|
||
connectionMode: "quick-connect",
|
||
source: "retained-current-process",
|
||
serverBound: true,
|
||
expectedDiscoveryGeneration: null,
|
||
},
|
||
);
|
||
|
||
state.network_mutation_ledger = {
|
||
status: "resolved",
|
||
mutation_allowed: true,
|
||
};
|
||
state.devices = [{
|
||
device_id: "durable-policy-k1",
|
||
name: "Lixel K1",
|
||
connectable: true,
|
||
}];
|
||
state.connection_policy.allowed_actions = [
|
||
"observe-fresh-device-network",
|
||
"observe-configured-device-network",
|
||
];
|
||
state.connection_policy.actions["observe-fresh-device-network"] = decision(
|
||
true,
|
||
"fresh-scan",
|
||
null,
|
||
null,
|
||
);
|
||
state.connection_policy.actions["observe-current-device-network"] = decision(
|
||
false,
|
||
"retained-current-process",
|
||
"retained-policy-k1",
|
||
"quick-connect",
|
||
);
|
||
assert.deepEqual(
|
||
lifecycle.readOnlyConnectionObservationTarget(
|
||
state,
|
||
"stale-browser-selection",
|
||
"direct-connect",
|
||
),
|
||
{
|
||
action: "observe-configured-device-network",
|
||
deviceId: "durable-policy-k1",
|
||
connectionMode: "bridge",
|
||
source: "durable-configured-state",
|
||
serverBound: true,
|
||
expectedDiscoveryGeneration: null,
|
||
},
|
||
);
|
||
assert.deepEqual(
|
||
lifecycle.serverBoundAppliedNetworkObservationTarget(state, "bridge"),
|
||
{
|
||
action: "observe-configured-device-network",
|
||
deviceId: "durable-policy-k1",
|
||
connectionMode: "bridge",
|
||
source: "durable-configured-state",
|
||
serverBound: true,
|
||
expectedDiscoveryGeneration: null,
|
||
},
|
||
);
|
||
});
|
||
|
||
test("legacy unresolved backend evidence never falls back to stale browser state", () => {
|
||
const state = {
|
||
devices: [],
|
||
network_mutation_ledger: {
|
||
status: "unresolved",
|
||
mutation_allowed: false,
|
||
},
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
allowed_actions: ["observe-configured-device-network"],
|
||
actions: {
|
||
"observe-configured-device-network": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: "durable-policy-k1",
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
facts: { retained_context_is_presence: false },
|
||
},
|
||
};
|
||
|
||
assert.equal(
|
||
lifecycle.readOnlyConnectionObservationTarget(
|
||
state,
|
||
"stale-browser-k1",
|
||
"quick-connect",
|
||
),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test("local receiver cleanup never targets a replay session's retained acquisition", () => {
|
||
assert.deepEqual(lifecycle.localReceiverStopPlan({
|
||
source_mode: "replay",
|
||
acquisition: {
|
||
acquisition_id: "terminal-live-acquisition",
|
||
state: "completed",
|
||
cleanup_pending: false,
|
||
},
|
||
}), { kind: "compatibility" });
|
||
|
||
assert.deepEqual(lifecycle.localReceiverStopPlan({
|
||
source_mode: "idle",
|
||
acquisition: {
|
||
acquisition_id: " exact-cleanup-target ",
|
||
state: "failed",
|
||
cleanup_pending: true,
|
||
},
|
||
}), {
|
||
kind: "acquisition",
|
||
acquisitionId: "exact-cleanup-target",
|
||
});
|
||
|
||
assert.deepEqual(lifecycle.localReceiverStopPlan({
|
||
source_mode: "idle",
|
||
acquisition: {
|
||
acquisition_id: "released-terminal-acquisition",
|
||
state: "failed",
|
||
cleanup_pending: false,
|
||
},
|
||
}), { kind: "compatibility" });
|
||
});
|
||
|
||
test("local receiver policy denial distinguishes proven idle from active cleanup", () => {
|
||
assert.equal(lifecycle.isProvenLocalReceiverInactive({
|
||
source_mode: "idle",
|
||
acquisition: null,
|
||
}), true);
|
||
assert.equal(lifecycle.isProvenLocalReceiverInactive({
|
||
source_mode: "idle",
|
||
acquisition: {
|
||
state: "failed",
|
||
cleanup_pending: false,
|
||
},
|
||
}), true);
|
||
assert.equal(lifecycle.isProvenLocalReceiverInactive({
|
||
source_mode: "idle",
|
||
acquisition: {
|
||
state: "failed",
|
||
cleanup_pending: true,
|
||
},
|
||
}), false);
|
||
assert.equal(lifecycle.isProvenLocalReceiverInactive({
|
||
source_mode: "live",
|
||
acquisition: {
|
||
state: "acquiring",
|
||
cleanup_pending: false,
|
||
},
|
||
}), false);
|
||
assert.equal(lifecycle.isProvenLocalReceiverInactive({
|
||
source_mode: "replay",
|
||
acquisition: {
|
||
state: "completed",
|
||
cleanup_pending: false,
|
||
},
|
||
}), false);
|
||
});
|
||
|
||
test("trusted K1 recovery uses only one exact backend-owned device and mode", () => {
|
||
const state = {
|
||
physical_command: {
|
||
status: "unresolved",
|
||
requires_reconciliation: true,
|
||
resolved_active_recovery_required: false,
|
||
},
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
allowed_actions: ["observe-configured-device-network"],
|
||
actions: {
|
||
"provision-fresh-device": {
|
||
allowed: false,
|
||
reason_codes: ["physical-command-reconciliation-required"],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: "physical-k1",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
"observe-configured-device-network": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: "physical-k1",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
facts: { retained_context_is_presence: false },
|
||
},
|
||
semantic_topology_store: {
|
||
record: {
|
||
transport_ref: "semantic-k1",
|
||
connection_mode: "quick-connect",
|
||
},
|
||
},
|
||
current_device_recovery: {
|
||
transport_ref: "process-k1",
|
||
connection_mode: "direct-connect",
|
||
},
|
||
};
|
||
|
||
assert.deepEqual(lifecycle.trustedConnectionBinding(state), {
|
||
deviceId: "physical-k1",
|
||
connectionMode: "bridge",
|
||
});
|
||
|
||
delete state.connection_policy.actions["provision-fresh-device"];
|
||
state.physical_command.requires_reconciliation = false;
|
||
assert.deepEqual(lifecycle.trustedConnectionBinding(state), {
|
||
deviceId: "semantic-k1",
|
||
connectionMode: "quick-connect",
|
||
});
|
||
|
||
state.semantic_topology_store.record.connection_mode = null;
|
||
assert.deepEqual(lifecycle.trustedConnectionBinding(state), {
|
||
deviceId: "process-k1",
|
||
connectionMode: "direct-connect",
|
||
});
|
||
|
||
state.current_device_recovery.connection_mode = null;
|
||
assert.equal(lifecycle.trustedConnectionBinding(state), null);
|
||
});
|
||
|
||
test("only an unresolved durable STOP blocks connection controls during backend cleanup", () => {
|
||
const unresolvedStop = {
|
||
source_mode: "live",
|
||
acquisition: {
|
||
state: "stopping",
|
||
cleanup_pending: true,
|
||
},
|
||
application_control_session: {
|
||
state: "awaiting-standby-confirmation",
|
||
physical_command: {
|
||
status: "unresolved",
|
||
requires_reconciliation: true,
|
||
record: {
|
||
action: "stop",
|
||
stage: "requested",
|
||
resolution: null,
|
||
},
|
||
},
|
||
},
|
||
};
|
||
assert.equal(lifecycle.isPhysicalStopRecoverySettling(unresolvedStop), true);
|
||
|
||
const boundedTimeout = structuredClone(unresolvedStop);
|
||
boundedTimeout.source_mode = "idle";
|
||
boundedTimeout.acquisition.state = "failed";
|
||
boundedTimeout.acquisition.cleanup_pending = false;
|
||
boundedTimeout.operations = [{
|
||
action: "acquisition.stop",
|
||
status: "timed_out",
|
||
}];
|
||
// The durable physical ledger remains intentionally unresolved, but all
|
||
// local work is released. The connection screen must leave the STOP loader
|
||
// and may start the separate bounded connection recovery path.
|
||
assert.equal(lifecycle.isPhysicalStopRecoverySettling(boundedTimeout), false);
|
||
|
||
const backendObservedStandby = structuredClone(unresolvedStop);
|
||
backendObservedStandby.application_control_session.state = "idle";
|
||
backendObservedStandby.application_control_session.physical_command.status = "resolved";
|
||
backendObservedStandby.application_control_session.physical_command.requires_reconciliation = false;
|
||
backendObservedStandby.application_control_session.physical_command.record.stage = "resolved";
|
||
backendObservedStandby.application_control_session.physical_command.record.resolution =
|
||
"stop-standby-observed";
|
||
assert.equal(
|
||
lifecycle.isPhysicalStopRecoverySettling(backendObservedStandby),
|
||
false,
|
||
);
|
||
|
||
const unresolvedStart = structuredClone(unresolvedStop);
|
||
unresolvedStart.application_control_session.physical_command.record.action = "start";
|
||
assert.equal(lifecycle.isPhysicalStopRecoverySettling(unresolvedStart), false);
|
||
});
|
||
|
||
test("physical recovery stays pinned to the original K1 across a multi-device BLE scan", () => {
|
||
const state = {
|
||
physical_command: {
|
||
status: "unresolved",
|
||
requires_reconciliation: true,
|
||
resolved_active_recovery_required: false,
|
||
},
|
||
ble_discovery_generation: 9,
|
||
devices: [
|
||
{ device_id: "nearby-other-k1", name: "Nearby K1", connectable: true },
|
||
{ device_id: "original-k1", name: "Original K1", connectable: true },
|
||
],
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
allowed_actions: ["scan-ble", "observe-fresh-device-network"],
|
||
actions: {
|
||
"provision-fresh-device": {
|
||
allowed: false,
|
||
reason_codes: ["physical-device-already-active"],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: "original-k1",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
"observe-fresh-device-network": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: "original-k1",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
facts: { retained_context_is_presence: false },
|
||
},
|
||
};
|
||
|
||
assert.equal(lifecycle.requiresReadOnlyPhysicalRecovery(state), true);
|
||
assert.deepEqual(lifecycle.readOnlyPhysicalRecoveryBinding(state), {
|
||
deviceId: "original-k1",
|
||
connectionMode: "bridge",
|
||
});
|
||
assert.deepEqual(
|
||
lifecycle.readOnlyConnectionObservationTarget(
|
||
state,
|
||
"nearby-other-k1",
|
||
"quick-connect",
|
||
),
|
||
{
|
||
action: "observe-fresh-device-network",
|
||
deviceId: "original-k1",
|
||
connectionMode: "bridge",
|
||
source: "fresh-scan",
|
||
serverBound: true,
|
||
expectedDiscoveryGeneration: 9,
|
||
},
|
||
);
|
||
|
||
state.devices = [{ device_id: "nearby-other-k1", name: "Nearby K1", connectable: true }];
|
||
assert.equal(
|
||
lifecycle.readOnlyConnectionObservationTarget(
|
||
state,
|
||
"nearby-other-k1",
|
||
"quick-connect",
|
||
),
|
||
null,
|
||
"another nearby K1 must not become a fallback recovery target",
|
||
);
|
||
});
|
||
|
||
test("READY-classified STOP exits recovery UI while START follows backend authority", () => {
|
||
const readyPhysical = {
|
||
status: "resolved",
|
||
reason_code: null,
|
||
requires_reconciliation: false,
|
||
resolved_active_recovery_required: false,
|
||
observed_session_state: "ready",
|
||
record: {
|
||
action: "stop",
|
||
stage: "resolved",
|
||
resolution: "not-dispatched",
|
||
reconciled_physical_state: "standby",
|
||
},
|
||
};
|
||
const readySuccessor = {
|
||
physical_command: readyPhysical,
|
||
application_control_session: {
|
||
physical_command: readyPhysical,
|
||
},
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
allowed_actions: ["start-acquisition"],
|
||
actions: {
|
||
"start-acquisition": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-physical-command",
|
||
required_transport_ref: "original-k1",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
"provision-fresh-device": {
|
||
allowed: false,
|
||
reason_codes: ["physical-command-reconciliation-required"],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: "original-k1",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
facts: { retained_context_is_presence: false },
|
||
},
|
||
connection_lifecycle: {
|
||
schema_version: "missioncore.xgrids-k1-connection-lifecycle/v1",
|
||
mode_selection: {
|
||
allowed: false,
|
||
reason_codes: ["connection-mode-selection-physical-state-unsafe"],
|
||
automatic_retry: false,
|
||
},
|
||
allowed_actions: ["start-acquisition"],
|
||
},
|
||
};
|
||
|
||
assert.equal(lifecycle.requiresReadOnlyPhysicalRecovery(readySuccessor), false);
|
||
assert.equal(lifecycle.connectionPolicyAllows(readySuccessor, "start-acquisition"), true);
|
||
assert.equal(
|
||
lifecycle.connectionPolicyAllows(readySuccessor, "provision-fresh-device"),
|
||
false,
|
||
"the exact successor binding remains pinned against network mutation",
|
||
);
|
||
assert.equal(
|
||
lifecycle.canSelectConnectionMode(readySuccessor),
|
||
false,
|
||
"the exact successor binding remains pinned against mode changes",
|
||
);
|
||
|
||
const unresolved = structuredClone(readySuccessor);
|
||
unresolved.application_control_session.physical_command.requires_reconciliation = true;
|
||
assert.equal(lifecycle.requiresReadOnlyPhysicalRecovery(unresolved), true);
|
||
|
||
const scanOver = structuredClone(readySuccessor);
|
||
scanOver.application_control_session.physical_command.requires_reconciliation = true;
|
||
scanOver.application_control_session.physical_command.resolved_scan_over_recovery_required = true;
|
||
assert.equal(lifecycle.requiresReadOnlyPhysicalRecovery(scanOver), true);
|
||
|
||
const reconciledActive = structuredClone(readySuccessor);
|
||
reconciledActive.application_control_session.physical_command.resolved_active_recovery_required = true;
|
||
reconciledActive.application_control_session.physical_command.observed_session_state = "scanning";
|
||
assert.equal(lifecycle.requiresReadOnlyPhysicalRecovery(reconciledActive), true);
|
||
|
||
const reopened = structuredClone(readySuccessor);
|
||
reopened.application_control_session.physical_command.resolved_active_recovery_required = true;
|
||
reopened.application_control_session.physical_command.reopened_physical_state_recovery_required = true;
|
||
assert.equal(lifecycle.requiresReadOnlyPhysicalRecovery(reopened), true);
|
||
});
|
||
|
||
test("only a newer authoritative reachable lease resolves connection errors", () => {
|
||
const error = {
|
||
action: "connect",
|
||
runtimeId: "runtime-a",
|
||
leaseGeneration: 4,
|
||
};
|
||
const reachableBridge = supervisedConnectionState({ generation: 5 });
|
||
|
||
assert.equal(
|
||
lifecycle.authoritativeReachableLeaseSupersedesError(error, reachableBridge),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
lifecycle.authoritativeReachableLeaseSupersedesError(
|
||
{ ...error, action: "scan" },
|
||
reachableBridge,
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
lifecycle.authoritativeReachableLeaseSupersedesError(
|
||
error,
|
||
supervisedConnectionState({ generation: 4 }),
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
lifecycle.authoritativeReachableLeaseSupersedesError(
|
||
error,
|
||
{ ...reachableBridge, network_write_reconciliation: {} },
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
lifecycle.authoritativeReachableLeaseSupersedesError(
|
||
error,
|
||
{ ...reachableBridge, snapshot_runtime_id: "runtime-b" },
|
||
),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("connection-mode reset is explicit and CAS-fenced before the next flow", async () => {
|
||
const pipelineSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/components/K1ProvisioningPipeline.tsx",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
const connectionSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/XgridsK1Connection.tsx",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
|
||
assert.doesNotMatch(pipelineSource, /nextReachableConnectionModeSynchronization/);
|
||
assert.doesNotMatch(pipelineSource, /synchronizedLeaseKeyRef/);
|
||
assert.doesNotMatch(pipelineSource, /setConnectionMode\(/);
|
||
assert.match(connectionSource, /state\.desired_connection_mode/);
|
||
assert.doesNotMatch(connectionSource, /selectConnectionMode\(\{/);
|
||
assert.match(connectionSource, /desiredModeLocallyDirty\.current = mode !== state\?\.desired_connection_mode/);
|
||
assert.match(connectionSource, /setDesiredConnectionMode\(mode\)/);
|
||
const localModeChange = pipelineSource.slice(
|
||
pipelineSource.indexOf("const changeDesiredConnectionMode"),
|
||
pipelineSource.indexOf("const selectFreshDevice"),
|
||
);
|
||
const explicitModeCommit = pipelineSource.slice(
|
||
pipelineSource.indexOf("const commitDesiredModeForExplicitAction"),
|
||
pipelineSource.indexOf("const currentPreparingReconfigurationRequest"),
|
||
);
|
||
assert.match(localModeChange, /await selectConnectionMode\(\{/);
|
||
assert.match(localModeChange, /expected_revision: expectedRevision as number/);
|
||
assert.match(localModeChange, /reset_scenario: true/);
|
||
assert.match(localModeChange, /reset_id: resetId/);
|
||
assert.doesNotMatch(
|
||
localModeChange,
|
||
/scanWithResult\(|connect\(|verifyConnection\(|prepareConnection/,
|
||
);
|
||
assert.match(explicitModeCommit, /selectConnectionMode\(\{/);
|
||
assert.match(explicitModeCommit, /expected_revision: expectedRevision as number/);
|
||
assert.match(
|
||
pipelineSource,
|
||
/const repeatDeviceScan[\s\S]*?await commitDesiredModeForExplicitAction\(\)/,
|
||
);
|
||
assert.match(
|
||
pipelineSource,
|
||
/const submitConnect[\s\S]*?await commitDesiredModeForExplicitAction\(\)/,
|
||
);
|
||
});
|
||
|
||
test("connection-mode selector follows the authoritative backend admission", () => {
|
||
const ready = supervisedConnectionState({ connectionReady: true });
|
||
assert.equal(lifecycle.canSelectConnectionMode(ready), true);
|
||
|
||
const prepared = structuredClone(ready);
|
||
prepared.acquisition = {
|
||
acquisition_id: "acq-prepared",
|
||
state: "prepared",
|
||
project_name: "TEST001",
|
||
};
|
||
assert.equal(lifecycle.canSelectConnectionMode(prepared), true);
|
||
|
||
for (const reasonCode of [
|
||
"connection-mode-selection-control-state-unsafe",
|
||
"connection-mode-selection-acquisition-active",
|
||
"connection-mode-selection-physical-state-unsafe",
|
||
]) {
|
||
const blocked = structuredClone(ready);
|
||
blocked.connection_lifecycle.mode_selection = {
|
||
allowed: false,
|
||
reason_codes: [reasonCode],
|
||
automatic_retry: false,
|
||
};
|
||
blocked.connection_lifecycle.allowed_actions = ["start-acquisition"];
|
||
assert.equal(lifecycle.canSelectConnectionMode(blocked), false);
|
||
}
|
||
|
||
const drifted = structuredClone(ready);
|
||
drifted.connection_lifecycle.allowed_actions = ["start-acquisition"];
|
||
assert.equal(lifecycle.canSelectConnectionMode(drifted), false);
|
||
assert.equal(lifecycle.canSelectConnectionMode({}), false);
|
||
});
|
||
|
||
test("prepared project survives a mode draft and its cancellation", () => {
|
||
assert.equal(projectName.shouldHydratePreparedProject({
|
||
acquisitionId: "acq-prepared",
|
||
hydratedAcquisitionId: null,
|
||
modeSwitchRequired: false,
|
||
}), true);
|
||
let displayedProject = projectName.projectNameAfterConnectionModeSelection("TEST001");
|
||
assert.equal(displayedProject, "TEST001");
|
||
assert.equal(projectName.validateProjectName(displayedProject).error, null);
|
||
|
||
assert.equal(projectName.shouldHydratePreparedProject({
|
||
acquisitionId: "acq-prepared",
|
||
hydratedAcquisitionId: "acq-prepared",
|
||
modeSwitchRequired: true,
|
||
}), false);
|
||
displayedProject = projectName.projectNameAfterConnectionModeSelection("TEST001");
|
||
assert.equal(displayedProject, "TEST001");
|
||
assert.equal(projectName.validateProjectName(displayedProject).error, null);
|
||
|
||
assert.equal(projectName.shouldHydratePreparedProject({
|
||
acquisitionId: "acq-prepared",
|
||
hydratedAcquisitionId: "acq-prepared",
|
||
modeSwitchRequired: false,
|
||
}), true);
|
||
|
||
assert.equal(projectName.projectNameAfterConnectionModeSelection(null), "");
|
||
});
|
||
|
||
test("one in-flight provisioning call keeps its idempotency key internally", () => {
|
||
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, side_effect_status: "unknown" },
|
||
};
|
||
const safePreWriteFailure = {
|
||
status: "failed",
|
||
error: { safe_to_retry: true, side_effect_status: "none" },
|
||
};
|
||
|
||
assert.equal(first, "network-provision:11111111-1111-4111-8111-111111111111");
|
||
assert.equal(repeated, first);
|
||
assert.equal(created, 1);
|
||
assert.equal(lifecycle.operationNeedsReconciliation(failedOperation), true);
|
||
assert.equal(
|
||
lifecycle.operationAllowsFreshProvisioningIntent(safePreWriteFailure),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
lifecycle.operationAllowsFreshProvisioningIntent(failedOperation),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("read-only verification releases only a durably resolved matching write fence", () => {
|
||
const fence = {
|
||
operation_id: "op-ambiguous",
|
||
transport_ref: "k1-a",
|
||
};
|
||
const before = {
|
||
snapshot_runtime_id: "runtime-a",
|
||
network_write_reconciliation: fence,
|
||
};
|
||
const resolvedLedger = {
|
||
status: "resolved",
|
||
mutation_allowed: true,
|
||
operation_id: "op-ambiguous",
|
||
stage: "resolved",
|
||
resolution: "target-observed",
|
||
};
|
||
|
||
assert.equal(
|
||
lifecycle.readOnlyVerificationClearedReconciliation(
|
||
before,
|
||
{
|
||
snapshot_runtime_id: "runtime-restarted",
|
||
network_write_reconciliation: null,
|
||
network_mutation_ledger: resolvedLedger,
|
||
},
|
||
"k1-a",
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
lifecycle.readOnlyVerificationClearedReconciliation(
|
||
before,
|
||
{
|
||
network_write_reconciliation: null,
|
||
network_mutation_ledger: {
|
||
status: "empty",
|
||
mutation_allowed: true,
|
||
operation_id: null,
|
||
stage: null,
|
||
resolution: null,
|
||
},
|
||
},
|
||
"k1-a",
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
lifecycle.readOnlyVerificationClearedReconciliation(before, {}, "k1-a"),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
lifecycle.readOnlyVerificationClearedReconciliation(
|
||
before,
|
||
{
|
||
network_write_reconciliation: fence,
|
||
network_mutation_ledger: {
|
||
...resolvedLedger,
|
||
status: "unresolved",
|
||
mutation_allowed: false,
|
||
stage: "observing",
|
||
resolution: null,
|
||
},
|
||
},
|
||
"k1-a",
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
lifecycle.readOnlyVerificationClearedReconciliation(
|
||
before,
|
||
{
|
||
network_write_reconciliation: null,
|
||
network_mutation_ledger: resolvedLedger,
|
||
},
|
||
"k1-b",
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
lifecycle.readOnlyVerificationClearedReconciliation(
|
||
before,
|
||
{
|
||
network_write_reconciliation: null,
|
||
network_mutation_ledger: {
|
||
...resolvedLedger,
|
||
operation_id: "op-different",
|
||
},
|
||
},
|
||
"k1-a",
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
lifecycle.readOnlyVerificationClearedReconciliation(
|
||
before,
|
||
{
|
||
network_write_reconciliation: null,
|
||
network_mutation_ledger: {
|
||
...resolvedLedger,
|
||
resolution: null,
|
||
},
|
||
},
|
||
"k1-a",
|
||
),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("each terminal explicit provisioning click starts a fresh operation identity", async () => {
|
||
const hookSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
const pipelineSource = await readFile(
|
||
new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/components/K1ProvisioningPipeline.tsx",
|
||
import.meta.url,
|
||
),
|
||
"utf8",
|
||
);
|
||
const connectRecovery = hookSource.slice(
|
||
hookSource.indexOf("const connect = useCallback"),
|
||
hookSource.indexOf("const verifyConnection = useCallback"),
|
||
);
|
||
const submitConnect = pipelineSource.slice(
|
||
pipelineSource.indexOf("const submitConnect = async"),
|
||
pipelineSource.indexOf("return (", pipelineSource.indexOf("const submitConnect = async")),
|
||
);
|
||
|
||
assert.match(
|
||
connectRecovery,
|
||
/observeProvisioningRequest\([\s\S]*?observedState = observation\.state[\s\S]*?const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted\(/,
|
||
);
|
||
assert.match(
|
||
connectRecovery,
|
||
/const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted\([\s\S]*?nextState = acceptSuccessfulConnectState\(nextState\)/,
|
||
);
|
||
assert.match(connectRecovery, /acceptedBleSessionKeyAfterConnect\(/);
|
||
assert.match(
|
||
connectRecovery,
|
||
/acceptedSessionKey:\s*networkIntentCompleted \? acceptedSessionKey : null/,
|
||
);
|
||
assert.match(submitConnect, /provisioningIntentKey\(null\)/);
|
||
assert.doesNotMatch(submitConnect, /provisioningIntentRef/);
|
||
assert.match(
|
||
submitConnect,
|
||
/const freshStartAllowed = result\.intentDisposition === "release"/,
|
||
);
|
||
assert.match(
|
||
submitConnect,
|
||
/setExplicitProvisioningDraft\(null\);[\s\S]*?setPassword\(""\);[\s\S]*?await connect\(/,
|
||
);
|
||
assert.match(submitConnect, /setSelectedDeviceSnapshot\(null\)/);
|
||
assert.doesNotMatch(pipelineSource, /Проверить K1 без записи/);
|
||
});
|
||
|
||
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({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-test",
|
||
device_id: "ble-device",
|
||
ssid: "lab-network",
|
||
password: syntheticCredential,
|
||
connection_mode: "bridge",
|
||
compatibility_attestation: attestation,
|
||
idempotency_key: "network-provision:test",
|
||
expected_mode_revision: 4,
|
||
expected_discovery_generation: 9,
|
||
});
|
||
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.equal(provisioning.input.expected_mode_revision, 4);
|
||
assert.equal(provisioning.input.expected_discovery_generation, 9);
|
||
assert.equal(
|
||
provisioning.input.expected_snapshot_runtime_id,
|
||
"snapshot-runtime-test",
|
||
);
|
||
assert.deepEqual(prepare.input.compatibility_attestation, attestation);
|
||
assert.equal(prepare.input.project_name, "Mission 01");
|
||
});
|
||
|
||
test("endpoint probing is separate from BLE refresh and 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({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-test",
|
||
device_id: "fresh-ble-device",
|
||
source: "fresh-scan",
|
||
compatibility_attestation: attestation,
|
||
expected_discovery_generation: 11,
|
||
});
|
||
await xgridsK1Api.verifyConnection({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-test",
|
||
device_id: "durable-ble-device",
|
||
source: "durable-configured-state",
|
||
compatibility_attestation: attestation,
|
||
});
|
||
await xgridsK1Api.probeConfiguredEndpoint({
|
||
expected_snapshot_runtime_id: "snapshot-runtime-test",
|
||
});
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
|
||
assert.equal(calls.length, 3);
|
||
assert.match(String(calls[0].path), /actions\/connection\.verify$/);
|
||
const adoption = JSON.parse(calls[0].init.body).input;
|
||
assert.deepEqual(adoption, {
|
||
expected_snapshot_runtime_id: "snapshot-runtime-test",
|
||
device_id: "fresh-ble-device",
|
||
source: "fresh-scan",
|
||
compatibility_attestation: attestation,
|
||
expected_discovery_generation: 11,
|
||
});
|
||
assert.equal("ssid" in adoption, false);
|
||
assert.equal("password" in adoption, false);
|
||
assert.equal("connection_mode" in adoption, false);
|
||
const durableAdoption = JSON.parse(calls[1].init.body).input;
|
||
assert.deepEqual(durableAdoption, {
|
||
expected_snapshot_runtime_id: "snapshot-runtime-test",
|
||
device_id: "durable-ble-device",
|
||
source: "durable-configured-state",
|
||
compatibility_attestation: attestation,
|
||
});
|
||
assert.equal("expected_discovery_generation" in durableAdoption, false);
|
||
assert.match(String(calls[2].path), /actions\/connection\.endpoint-probe$/);
|
||
assert.deepEqual(JSON.parse(calls[2].init.body), {
|
||
input: { expected_snapshot_runtime_id: "snapshot-runtime-test" },
|
||
});
|
||
});
|
||
|
||
test("connection verification accepts only the backend literal status and lease contract", () => {
|
||
const statuses = [
|
||
"not-probed",
|
||
"device-network-applied",
|
||
"device-network-applied-host-failed",
|
||
"adopted",
|
||
"host-route-mismatch",
|
||
"endpoint-unreachable",
|
||
"tcp-reachable-device-info-unverified",
|
||
"reachable",
|
||
"recovered",
|
||
"control-transport-lost",
|
||
"unreachable",
|
||
];
|
||
const leaseStates = ["disconnected", "configured-unverified", "reachable"];
|
||
assert.deepEqual([...XGRIDS_CONNECTION_VERIFICATION_STATUSES], statuses);
|
||
assert.deepEqual([...XGRIDS_CONNECTION_VERIFICATION_LEASE_STATES], leaseStates);
|
||
|
||
const exact = {
|
||
status: "not-probed",
|
||
lease_state: "disconnected",
|
||
lease_generation: 0,
|
||
supervisor_revision: 1,
|
||
endpoint_validation: "not-performed",
|
||
network_reachability: "unknown",
|
||
observed_at: null,
|
||
};
|
||
for (const status of statuses) {
|
||
assert.equal(isXgridsConnectionVerification({ ...exact, status }), true, status);
|
||
}
|
||
for (const lease_state of leaseStates) {
|
||
assert.equal(
|
||
isXgridsConnectionVerification({ ...exact, lease_state }),
|
||
true,
|
||
lease_state,
|
||
);
|
||
}
|
||
|
||
for (const malformed of [
|
||
{ ...exact, status: "configured" },
|
||
{ ...exact, lease_state: "lost" },
|
||
{ ...exact, lease_generation: -1 },
|
||
{ ...exact, supervisor_revision: 1.25 },
|
||
{ ...exact, network_reachability: "degraded" },
|
||
{ ...exact, observed_at: 123 },
|
||
Object.fromEntries(Object.entries(exact).filter(([key]) => key !== "status")),
|
||
Object.fromEntries(Object.entries(exact).filter(([key]) => key !== "lease_state")),
|
||
]) {
|
||
assert.equal(isXgridsConnectionVerification(malformed), false);
|
||
}
|
||
});
|
||
|
||
test("connection policy accepts the exact restart-recovery actions, sources and modes", () => {
|
||
assert.deepEqual([...XGRIDS_CONNECTION_POLICY_ACTIONS], [
|
||
"scan-ble",
|
||
"provision-fresh-device",
|
||
"prepare-select-device",
|
||
"prepare-change-network",
|
||
"cancel-reconfiguration",
|
||
"recover-current-device-network",
|
||
"observe-fresh-device-network",
|
||
"observe-current-device-network",
|
||
"observe-configured-device-network",
|
||
"inspect-configured-endpoint",
|
||
"inspect-host-network",
|
||
"probe-endpoint",
|
||
"verify-control-device-info",
|
||
"start-acquisition",
|
||
"stop-acquisition",
|
||
"stop-local-receiver",
|
||
"retire-unavailable-physical-target",
|
||
"acknowledge-data-loss",
|
||
]);
|
||
assert.deepEqual([...XGRIDS_CONNECTION_POLICY_TARGET_SOURCES], [
|
||
"none",
|
||
"fresh-scan",
|
||
"retained-current-process",
|
||
"durable-configured-state",
|
||
"configured-topology",
|
||
"connection-supervisor",
|
||
"local-runtime",
|
||
"local-prestart-handoff",
|
||
"local-reconfiguration-intent",
|
||
"durable-physical-command",
|
||
]);
|
||
|
||
const retainedDecision = {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "retained-current-process",
|
||
required_transport_ref: "9AE978F2-37A8-4B4B-9BCD-BD7010BB20D1",
|
||
required_connection_mode: "quick-connect",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
};
|
||
const durableDecision = {
|
||
...retainedDecision,
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: "0CA8AB68-E37B-46A2-B09F-C34B5C49428C",
|
||
required_connection_mode: "bridge",
|
||
};
|
||
const policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
supervisor_revision: 17,
|
||
network_ledger_revision: 4,
|
||
recommended_action: "observe-current-device-network",
|
||
allowed_actions: [
|
||
"observe-current-device-network",
|
||
"observe-configured-device-network",
|
||
],
|
||
actions: {
|
||
"observe-current-device-network": retainedDecision,
|
||
"observe-configured-device-network": durableDecision,
|
||
},
|
||
facts: { retained_context_is_presence: false },
|
||
};
|
||
|
||
assert.equal(isXgridsConnectionPolicyDecision(retainedDecision), true);
|
||
assert.equal(isXgridsConnectionPolicy(policy), true);
|
||
const retirementDecision = {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-physical-command",
|
||
required_transport_ref: "0CA8AB68-E37B-46A2-B09F-C34B5C49428C",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: false,
|
||
physical_command_allowed: false,
|
||
physical_outcome: "unknown",
|
||
device_write_performed: false,
|
||
automatic_retry: false,
|
||
};
|
||
assert.equal(isXgridsConnectionPolicyDecision(retirementDecision), true);
|
||
assert.equal(isXgridsConnectionPolicy({
|
||
...policy,
|
||
recommended_action: "retire-unavailable-physical-target",
|
||
allowed_actions: ["retire-unavailable-physical-target"],
|
||
actions: {
|
||
"retire-unavailable-physical-target": retirementDecision,
|
||
},
|
||
}), true);
|
||
for (const malformedRetirement of [
|
||
{ ...retirementDecision, target_source: "durable-configured-state" },
|
||
{ ...retirementDecision, physical_command_allowed: undefined },
|
||
{ ...retirementDecision, physical_outcome: undefined },
|
||
{ ...retirementDecision, device_write_performed: undefined },
|
||
{ ...retirementDecision, requires_live_gatt_validation: true },
|
||
]) {
|
||
assert.equal(isXgridsConnectionPolicy({
|
||
...policy,
|
||
recommended_action: "retire-unavailable-physical-target",
|
||
allowed_actions: ["retire-unavailable-physical-target"],
|
||
actions: {
|
||
"retire-unavailable-physical-target": malformedRetirement,
|
||
},
|
||
}), false);
|
||
}
|
||
assert.equal(
|
||
isXgridsConnectionPolicyDecision({
|
||
...retainedDecision,
|
||
required_connection_mode: undefined,
|
||
}),
|
||
true,
|
||
"older decisions may omit the optional mode field",
|
||
);
|
||
|
||
for (const malformed of [
|
||
{ ...retainedDecision, target_source: "server-cache" },
|
||
{ ...retainedDecision, required_connection_mode: "automatic" },
|
||
{ ...retainedDecision, automatic_retry: true },
|
||
]) {
|
||
assert.equal(isXgridsConnectionPolicyDecision(malformed), false);
|
||
}
|
||
assert.equal(isXgridsConnectionPolicy({
|
||
...policy,
|
||
actions: {
|
||
...policy.actions,
|
||
"observe-current-device-network": {
|
||
...retainedDecision,
|
||
required_connection_mode: undefined,
|
||
},
|
||
},
|
||
}), false, "an allowed retained recovery must pin its mode");
|
||
assert.equal(isXgridsConnectionPolicy({
|
||
...policy,
|
||
actions: {
|
||
...policy.actions,
|
||
"observe-configured-device-network": {
|
||
...durableDecision,
|
||
target_source: "configured-topology",
|
||
},
|
||
},
|
||
}), false, "durable recovery cannot drift to an endpoint-only source");
|
||
});
|
||
|
||
test("connection reconfiguration accepts only the exact resumable v1 projection", () => {
|
||
const exact = {
|
||
schema_version: "missioncore.xgrids-k1-connection-reconfiguration/v1",
|
||
revision: 4,
|
||
intent_id: "connection-reconfigure-001",
|
||
intent: "change-network",
|
||
status: "fresh-scan-completed",
|
||
required_transport_ref: "BLE-DEVICE-001",
|
||
required_connection_mode: "bridge",
|
||
minimum_discovery_generation: 10,
|
||
fresh_discovery_generation: 10,
|
||
required_transport_observed: true,
|
||
prepared_at: "2026-08-10T12:00:00Z",
|
||
automatic_retry: false,
|
||
};
|
||
assert.equal(isXgridsConnectionReconfiguration(exact), true);
|
||
assert.equal(isXgridsConnectionReconfiguration({
|
||
...exact,
|
||
status: "idle",
|
||
intent: null,
|
||
intent_id: null,
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
minimum_discovery_generation: null,
|
||
fresh_discovery_generation: null,
|
||
required_transport_observed: null,
|
||
prepared_at: null,
|
||
}), true);
|
||
for (const malformed of [
|
||
{ ...exact, revision: -1 },
|
||
{ ...exact, intent: "cancel" },
|
||
{ ...exact, status: "scanning" },
|
||
{ ...exact, required_connection_mode: "bluetooth" },
|
||
{ ...exact, automatic_retry: true },
|
||
{ ...exact, intent_id: null },
|
||
]) {
|
||
assert.equal(isXgridsConnectionReconfiguration(malformed), false);
|
||
}
|
||
});
|
||
|
||
test("connection attempt phase distinguishes no write from an unknown write outcome", async () => {
|
||
assert.deepEqual([...XGRIDS_CONNECTION_ATTEMPT_PHASES], [
|
||
"network_applied",
|
||
"network_not_applied",
|
||
"network_outcome_unknown",
|
||
]);
|
||
for (const phase of XGRIDS_CONNECTION_ATTEMPT_PHASES) {
|
||
assert.equal(isXgridsConnectionAttemptPhase(phase), true, phase);
|
||
}
|
||
for (const phase of [
|
||
"network_write_failed",
|
||
"network_not_confirmed",
|
||
"outcome_unknown",
|
||
null,
|
||
]) {
|
||
assert.equal(isXgridsConnectionAttemptPhase(phase), false, String(phase));
|
||
}
|
||
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = async () => new Response(JSON.stringify({
|
||
state: {
|
||
source_mode: "idle",
|
||
connection_attempt: {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
phase: "network_outcome_unknown",
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
}), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
|
||
try {
|
||
const state = await xgridsK1Api.getState();
|
||
assert.equal(state.connection_attempt.phase, "network_outcome_unknown");
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("state API rejects an unreviewed connection attempt phase", async () => {
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = async () => new Response(JSON.stringify({
|
||
state: {
|
||
source_mode: "idle",
|
||
connection_attempt: {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
phase: "network_write_failed",
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
}), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
|
||
try {
|
||
await assert.rejects(
|
||
xgridsK1Api.getState(),
|
||
(error) => error instanceof ApiError
|
||
&& error.message === "Локальный сервер вернул некорректное состояние.",
|
||
);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("state API rejects a drifted connection verification object at runtime", async () => {
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = async () => new Response(JSON.stringify({
|
||
state: {
|
||
source_mode: "idle",
|
||
connection_verification: {
|
||
status: "configured",
|
||
lease_state: "lost",
|
||
lease_generation: 1,
|
||
supervisor_revision: 2,
|
||
endpoint_validation: "not-performed",
|
||
network_reachability: "degraded",
|
||
observed_at: null,
|
||
},
|
||
},
|
||
}), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
|
||
try {
|
||
await assert.rejects(
|
||
xgridsK1Api.getState(),
|
||
(error) => error instanceof ApiError
|
||
&& error.message === "Локальный сервер вернул некорректное состояние.",
|
||
);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("state API rejects a drifted restart-recovery policy at runtime", async () => {
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = async () => new Response(JSON.stringify({
|
||
state: {
|
||
source_mode: "idle",
|
||
connection_policy: {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
supervisor_revision: 5,
|
||
network_ledger_revision: 3,
|
||
recommended_action: "observe-configured-device-network",
|
||
allowed_actions: ["observe-configured-device-network"],
|
||
actions: {
|
||
"observe-configured-device-network": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: "exact-k1",
|
||
required_connection_mode: "automatic",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
facts: { retained_context_is_presence: false },
|
||
},
|
||
},
|
||
}), {
|
||
status: 200,
|
||
headers: { "Content-Type": "application/json" },
|
||
});
|
||
|
||
try {
|
||
await assert.rejects(
|
||
xgridsK1Api.getState(),
|
||
(error) => error instanceof ApiError
|
||
&& error.message === "Локальный сервер вернул некорректное состояние.",
|
||
);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("host diagnostic guard rejects every unreviewed literal dimension", () => {
|
||
const exact = {
|
||
schema_version: "missioncore.host-failure-diagnostic/v1",
|
||
code: "host.keychain.interaction-required",
|
||
domain: "keychain",
|
||
impact: "control",
|
||
operator_action: "unlock-or-authorize-keychain",
|
||
automatic_retry: false,
|
||
redacted: true,
|
||
};
|
||
assert.equal(isXgridsHostFailureDiagnostic(exact), true);
|
||
for (const malformed of [
|
||
{ ...exact, code: "PermissionError: private" },
|
||
{ ...exact, domain: "python-runtime" },
|
||
{ ...exact, impact: "unknown-impact" },
|
||
{ ...exact, operator_action: "run-private-shell-command" },
|
||
{ ...exact, automatic_retry: true },
|
||
{ ...exact, redacted: false },
|
||
]) {
|
||
assert.equal(isXgridsHostFailureDiagnostic(malformed), false);
|
||
}
|
||
});
|
||
|
||
test("failed network-profile writes close the UI session with a fresh explicit next step", () => {
|
||
const message = networkProvisionFailureMessage({
|
||
status: "failed",
|
||
error: { code: "BleakGATTProtocolError" },
|
||
});
|
||
|
||
assert.equal(
|
||
message,
|
||
"Bluetooth-периферия завершила сетевую операцию ошибкой. Команда могла быть принята K1; итог текущей попытки не подтверждён. Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth.",
|
||
);
|
||
assert.doesNotMatch(message, /Bleak|GATT|ATT/i);
|
||
|
||
const attMessage = networkProvisionFailureMessage({
|
||
action: "network.provision",
|
||
status: "failed",
|
||
error: {
|
||
code: "BleakGATTProtocolError",
|
||
ble_att_error_code: 4,
|
||
ble_att_error_name: "INVALID_PDU",
|
||
},
|
||
});
|
||
assert.match(attMessage, /ATT 4 INVALID_PDU/);
|
||
assert.match(attMessage, /Автоматический повтор команды K1 не отправлялся/);
|
||
assert.match(attMessage, /Проверьте состояние K1 через «Переподключиться»/);
|
||
assert.match(attMessage, /новое подключение через поиск Bluetooth/);
|
||
|
||
const preWriteAttMessage = networkProvisionFailureMessage({
|
||
action: "network.provision",
|
||
status: "failed",
|
||
error: {
|
||
code: "BleakGATTProtocolError",
|
||
operation_stage: "baseline-read",
|
||
device_write_attempted: false,
|
||
side_effect_status: "none",
|
||
safe_to_retry: true,
|
||
ble_att_error_code: 4,
|
||
ble_att_error_name: "INVALID_PDU",
|
||
},
|
||
});
|
||
assert.match(preWriteAttMessage, /до команды изменения сети/);
|
||
assert.match(preWriteAttMessage, /Запись сетевого профиля не выполнялась/);
|
||
assert.doesNotMatch(preWriteAttMessage, /результат изменения сети неизвестен/i);
|
||
});
|
||
|
||
test("post-dispatch ambiguity resets the UI session without an automatic retry", () => {
|
||
const message = networkProvisionFailureMessage({
|
||
action: "network.provision",
|
||
status: "failed",
|
||
error: {
|
||
code: "network-provision-target-not-distinguishable-from-baseline",
|
||
device_write_attempted: true,
|
||
side_effect_status: "unknown",
|
||
safe_to_retry: false,
|
||
},
|
||
});
|
||
|
||
assert.equal(
|
||
message,
|
||
"После BLE-команды K1 вернул сетевой статус, неотличимый от исходного; итог текущей попытки подключения не подтверждён. Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth.",
|
||
);
|
||
assert.doesNotMatch(message, /ручн|read-only|защитный барьер/i);
|
||
});
|
||
|
||
test("Bluetooth scan failures explain whether a device command was sent", () => {
|
||
const busy = discoveryScanFailureMessage({
|
||
action: "discovery.scan",
|
||
status: "failed",
|
||
error: { code: "ble-runtime-busy" },
|
||
});
|
||
const cleanupPending = discoveryScanFailureMessage({
|
||
action: "discovery.scan",
|
||
status: "failed",
|
||
error: { code: "ble-runtime-cleanup-pending" },
|
||
});
|
||
const timedOut = discoveryScanFailureMessage({
|
||
action: "discovery.scan",
|
||
status: "failed",
|
||
error: { code: "ble-discovery-timeout" },
|
||
});
|
||
|
||
assert.match(busy, /занят другой локальной операцией/);
|
||
assert.match(cleanupPending, /подтверждает отключение/);
|
||
assert.match(timedOut, /принудительно остановлен/);
|
||
assert.match(timedOut, /Команды K1 не отправлялись/);
|
||
});
|
||
|
||
test("connection recovery exposes safe operator-facing failure classes", () => {
|
||
const addressUnavailable = connectionVerificationFailureMessage({
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: {
|
||
code: "connection-verify-address-unavailable",
|
||
side_effect_status: "none",
|
||
safe_to_retry: true,
|
||
},
|
||
});
|
||
const missingAdvertisement = connectionVerificationFailureMessage({
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: { code: "connection-verify-device-not-rediscovered" },
|
||
});
|
||
const statusReadFailed = connectionVerificationFailureMessage({
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: {
|
||
code: "connection-verify-status-read-failed",
|
||
operation_stage: "exact-uuid-scan",
|
||
side_effect_status: "none",
|
||
},
|
||
});
|
||
const exactUuidScanTimedOut = connectionVerificationFailureMessage({
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: {
|
||
code: "connection-verify-exact-uuid-scan-timeout",
|
||
operation_stage: "exact-uuid-scan",
|
||
side_effect_status: "none",
|
||
},
|
||
});
|
||
const indistinguishableFromBaseline = connectionVerificationFailureMessage({
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: {
|
||
code: "connection-verify-target-not-distinguishable-from-baseline",
|
||
side_effect_status: "none",
|
||
safe_to_retry: false,
|
||
},
|
||
});
|
||
const staleEndpoint = connectionVerificationFailureMessage({
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: {
|
||
code: "connection-verify-mqtt-unreachable",
|
||
side_effect_status: "none",
|
||
safe_to_retry: true,
|
||
},
|
||
});
|
||
const bindingChanged = connectionVerificationFailureMessage({
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: {
|
||
code: "application-connection-binding-lost",
|
||
side_effect_status: "none",
|
||
safe_to_retry: true,
|
||
},
|
||
});
|
||
const physicalProofTimedOut = connectionVerificationFailureMessage({
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: {
|
||
code: "physical-command-reconciliation-proof-timeout",
|
||
side_effect_status: "none",
|
||
safe_to_retry: true,
|
||
},
|
||
});
|
||
|
||
assert.match(addressUnavailable, /K1 ответил/);
|
||
assert.match(addressUnavailable, /настройки устройства не менялись/);
|
||
assert.match(missingAdvertisement, /Mission Core не получил объявление/);
|
||
assert.match(missingAdvertisement, /Команды K1 не отправлялись/);
|
||
assert.doesNotMatch(missingAdvertisement, /питани|перезапуст|не работает|пропал/i);
|
||
assert.match(statusReadFailed, /Mission Core не завершил/);
|
||
assert.match(statusReadFailed, /команды не отправлялись/);
|
||
assert.doesNotMatch(statusReadFailed, /питани|перезапуст|не работает|K1 не ответил/i);
|
||
assert.match(exactUuidScanTimedOut, /точного сохранённого CoreBluetooth UUID/);
|
||
assert.match(exactUuidScanTimedOut, /не является выводом о состоянии устройства/);
|
||
assert.doesNotMatch(exactUuidScanTimedOut, /питани|перезапуст|не работает|K1 не ответил/i);
|
||
assert.match(staleEndpoint, /можно заново применить настройки общей сети/);
|
||
assert.match(staleEndpoint, /Команда Wi-Fi не отправлялась/);
|
||
assert.doesNotMatch(staleEndpoint, /новый поиск|read-only/i);
|
||
assert.match(bindingChanged, /нажмите «Подключиться заново»/);
|
||
assert.doesNotMatch(bindingChanged, /поиск Bluetooth/i);
|
||
assert.match(physicalProofTimedOut, /START и STOP не отправлялись/);
|
||
assert.match(physicalProofTimedOut, /нажмите «Подключиться заново»/);
|
||
assert.doesNotMatch(physicalProofTimedOut, /поиск Bluetooth/i);
|
||
assert.equal(
|
||
indistinguishableFromBaseline,
|
||
"K1 ответил, но приложение не смогло подтвердить, что прежние настройки сети были применены. Автоматического повтора и новой записи не было.",
|
||
);
|
||
assert.doesNotMatch(indistinguishableFromBaseline, /поиск Bluetooth|повторите запись/i);
|
||
});
|
||
|
||
test("scan and verify errors correlate only with the requested operation id", () => {
|
||
const state = {
|
||
operations: [
|
||
{
|
||
operation_id: "op-requested",
|
||
action: "discovery.scan",
|
||
status: "failed",
|
||
error: { code: "ble-discovery-timeout" },
|
||
},
|
||
{
|
||
operation_id: "op-other-tab",
|
||
action: "discovery.scan",
|
||
status: "failed",
|
||
error: { code: "ble-discovery-already-running" },
|
||
},
|
||
],
|
||
last_operation: {
|
||
operation_id: "op-other-tab",
|
||
action: "discovery.scan",
|
||
status: "failed",
|
||
},
|
||
};
|
||
|
||
assert.equal(
|
||
operationById(state, "discovery.scan", "op-requested")?.error?.code,
|
||
"ble-discovery-timeout",
|
||
);
|
||
assert.equal(operationById(state, "discovery.scan", "op-missing"), null);
|
||
});
|
||
|
||
test("host Wi-Fi failures close the attempt and require a fresh explicit connection", () => {
|
||
const operationTimeout = networkProvisionFailureMessage({
|
||
status: "failed",
|
||
error: { code: "host-wifi-operation-timeout" },
|
||
});
|
||
const missingNetwork = networkProvisionFailureMessage({
|
||
status: "failed",
|
||
error: {
|
||
code: "network-not-found",
|
||
side_effect_status: "confirmed",
|
||
safe_to_retry: false,
|
||
scan_attempt_count: 13,
|
||
scan_elapsed_ms: 17524,
|
||
},
|
||
});
|
||
const preWriteKeychain = networkProvisionFailureMessage({
|
||
status: "failed",
|
||
error: {
|
||
code: "keychain-authorization-required",
|
||
side_effect_status: "none",
|
||
safe_to_retry: true,
|
||
},
|
||
});
|
||
const postWriteKeychain = networkProvisionFailureMessage({
|
||
status: "failed",
|
||
error: {
|
||
code: "keychain-authorization-denied",
|
||
side_effect_status: "confirmed",
|
||
safe_to_retry: false,
|
||
},
|
||
});
|
||
|
||
assert.equal(
|
||
operationTimeout,
|
||
"Локальная операция подготовки Wi‑Fi не завершилась вовремя; итог текущей попытки подключения не подтверждён. Автоматический повтор команды K1 не отправлялся. Проверьте состояние K1 через «Переподключиться» или начните новое подключение через поиск Bluetooth.",
|
||
);
|
||
assert.doesNotMatch(operationTimeout, /получите пароль|пароль отсутствует/i);
|
||
assert.match(missingNetwork, /K1 принял команду Quick Connect/);
|
||
assert.match(missingNetwork, /13 проверок за 17\.5 с/);
|
||
assert.match(missingNetwork, /автоматического повтора не было/);
|
||
assert.match(preWriteKeychain, /Команда устройству не отправлялась/);
|
||
assert.match(postWriteKeychain, /Дополнительный пароль не запрашивался/);
|
||
});
|