7772 lines
271 KiB
JavaScript
7772 lines
271 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import { after, before, test } from "node:test";
|
||
|
||
import React, { createElement } from "react";
|
||
import { renderToStaticMarkup } from "react-dom/server";
|
||
import { createServer } from "vite";
|
||
|
||
let server;
|
||
let normalizeXgridsK1MissionState;
|
||
let xgridsK1Manifest;
|
||
let workspaces;
|
||
let connectionModeOptions;
|
||
let K1Metrics;
|
||
let K1ConnectionPipelines;
|
||
let physicalRecoveryConnectionDetail;
|
||
let shouldRenderK1OperationalPanels;
|
||
let K1ProvisioningPipeline;
|
||
let RuntimeActionFenceTestContext;
|
||
let LOCAL_OPERATION_PROGRESS_DELAY_MILLISECONDS;
|
||
let emptySearchPresentation;
|
||
let emptyProvisioningAttemptPresentation;
|
||
let emptyReadOnlyReconnectPresentation;
|
||
let provisioningAttemptViewState;
|
||
let unavailablePhysicalRetirementAuthority;
|
||
let connectionAttemptOwnsAppliedNetworkRecovery;
|
||
let connectionRecoveryObservationTargetMatches;
|
||
let connectionRecoveryEscapeKey;
|
||
let connectionRecoveryEscapeAfterScan;
|
||
let connectionRecoveryIsRequired;
|
||
let clearConnectionFailureAfterSuccessfulRefresh;
|
||
let dispatchConnectionRecoveryObservationForCurrentRuntime;
|
||
let dispatchUnavailablePhysicalRetirementForCurrentRuntime;
|
||
let dispatchRetiredPhysicalReconciliationForCurrentRuntime;
|
||
let retiredPhysicalReopenVerificationContext;
|
||
let admitPhysicalReopenPresentation;
|
||
let physicalReopenClickAuthority;
|
||
let physicalReopenPresentationIsCurrent;
|
||
let physicalReopenSettlementIsCurrent;
|
||
let scenarioResetPresentationBoundary;
|
||
let localScenarioActionEpochIsCurrent;
|
||
let explicitProvisioningDraftMatches;
|
||
let localProvisioningDraftFenceKey;
|
||
let exactChangeNetworkCandidate;
|
||
let connectionActionAuthorityMatches;
|
||
let reconfigurationContinuationAuthority;
|
||
let observedConnectionAuthorityAllowsTarget;
|
||
let connectContinuationAuthority;
|
||
let runtimeActionFenceMatches;
|
||
let currentRuntimeActionRequest;
|
||
let connectedReconfigurationActionApplicable;
|
||
let K1AcquisitionPipeline;
|
||
let K1Diagnostics;
|
||
let K1SpatialControlsView;
|
||
let k1SpatialAuthorityState;
|
||
let k1SpatialPhasePresentation;
|
||
let contourRuntimeAuthorityPresentation;
|
||
let connectionPolicyOperatorGuidance;
|
||
let connectionModeSelectionGuidance;
|
||
let physicalRetirementGuidance;
|
||
let physicalReopenGuidance;
|
||
let K1OperatorError;
|
||
let attemptNetworkPhaseLabel;
|
||
let hostFailureDiagnosticPresentation;
|
||
let shellPresentation;
|
||
let reconfigurationAllowsFreshDevice;
|
||
let readOnlyObservationShowsNetworkUnavailable;
|
||
let readOnlyFailureShowsNetworkUnavailable;
|
||
let savedBridgeRequiresNetworkSetup;
|
||
let isConnectionControlBootstrapSettling;
|
||
let provisioningFailureRequiresFreshCandidate;
|
||
let trustedConnectionBinding;
|
||
let transportRefEquivalenceKey;
|
||
let shouldRevealProvisioningNetworkStep;
|
||
let retiredPhysicalReopenAuthority;
|
||
let SnapshotRuntimeActionArbiter;
|
||
let runtimeActionResponseAlreadyAccepted;
|
||
let connectionActionAuthoritySnapshot;
|
||
let exactAppliedNetworkIntentCompleted;
|
||
let selectMonotonicXgridsState;
|
||
let canIssueCanonicalStop;
|
||
let physicalStopIntentCheckpoint;
|
||
let authoritativeStateSupersedesPhysicalStopIntent;
|
||
let connectionAttemptForRuntimeError;
|
||
let recommendedConnectionRecoveryObservationTarget;
|
||
let readOnlyConnectionObservationTarget;
|
||
|
||
const PROFILE_ID = "xgrids.lixelkity-k1.fw-3.0.2.local-network.v2";
|
||
const OBSERVED_AT = "2026-08-06T12:00:00Z";
|
||
const provisioningSourceUrl = new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/components/K1ProvisioningPipeline.tsx",
|
||
import.meta.url,
|
||
);
|
||
const provisioningStylesUrl = new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/styles.css",
|
||
import.meta.url,
|
||
);
|
||
|
||
before(async () => {
|
||
server = await createServer({
|
||
appType: "custom",
|
||
logLevel: "silent",
|
||
server: { middlewareMode: true },
|
||
});
|
||
({ normalizeXgridsK1MissionState } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/runtimeContext.tsx",
|
||
));
|
||
({
|
||
SnapshotRuntimeActionArbiter,
|
||
connectionActionAuthoritySnapshot,
|
||
exactAppliedNetworkIntentCompleted,
|
||
runtimeActionResponseAlreadyAccepted,
|
||
} = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/useXgridsK1Runtime.ts",
|
||
));
|
||
({ selectMonotonicXgridsState } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/stateOrdering.ts",
|
||
));
|
||
({ xgridsK1Manifest } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/manifest.ts",
|
||
));
|
||
({ workspaces } = await server.ssrLoadModule("/src/productModel.ts"));
|
||
({ connectionModeOptions } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/configuration.ts",
|
||
));
|
||
({ K1Metrics } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/components/K1Metrics.tsx",
|
||
));
|
||
({
|
||
K1ProvisioningPipeline,
|
||
RuntimeActionFenceTestContext,
|
||
LOCAL_OPERATION_PROGRESS_DELAY_MILLISECONDS,
|
||
emptySearchPresentation,
|
||
emptyProvisioningAttemptPresentation,
|
||
emptyReadOnlyReconnectPresentation,
|
||
provisioningAttemptViewState,
|
||
unavailablePhysicalRetirementAuthority,
|
||
connectionAttemptOwnsAppliedNetworkRecovery,
|
||
connectionRecoveryObservationTargetMatches,
|
||
connectionRecoveryEscapeKey,
|
||
connectionRecoveryEscapeAfterScan,
|
||
connectionRecoveryIsRequired,
|
||
clearConnectionFailureAfterSuccessfulRefresh,
|
||
dispatchConnectionRecoveryObservationForCurrentRuntime,
|
||
dispatchUnavailablePhysicalRetirementForCurrentRuntime,
|
||
dispatchRetiredPhysicalReconciliationForCurrentRuntime,
|
||
retiredPhysicalReopenVerificationContext,
|
||
admitPhysicalReopenPresentation,
|
||
physicalReopenClickAuthority,
|
||
physicalReopenPresentationIsCurrent,
|
||
physicalReopenSettlementIsCurrent,
|
||
scenarioResetPresentationBoundary,
|
||
localScenarioActionEpochIsCurrent,
|
||
explicitProvisioningDraftMatches,
|
||
localProvisioningDraftFenceKey,
|
||
exactChangeNetworkCandidate,
|
||
connectionActionAuthorityMatches,
|
||
reconfigurationContinuationAuthority,
|
||
observedConnectionAuthorityAllowsTarget,
|
||
connectContinuationAuthority,
|
||
runtimeActionFenceMatches,
|
||
currentRuntimeActionRequest,
|
||
connectedReconfigurationActionApplicable,
|
||
} = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/components/K1ProvisioningPipeline.tsx",
|
||
));
|
||
({ K1AcquisitionPipeline } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/components/K1AcquisitionPipeline.tsx",
|
||
));
|
||
({ K1Diagnostics } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/components/K1Diagnostics.tsx",
|
||
));
|
||
({
|
||
K1SpatialControlsView,
|
||
k1SpatialAuthorityState,
|
||
k1SpatialPhasePresentation,
|
||
} = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/components/K1SpatialControls.tsx",
|
||
));
|
||
({
|
||
connectionModeSelectionGuidance,
|
||
connectionPolicyOperatorGuidance,
|
||
physicalRetirementGuidance,
|
||
physicalReopenGuidance,
|
||
} = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/presentation.ts",
|
||
));
|
||
({
|
||
reconfigurationAllowsFreshDevice,
|
||
readOnlyObservationShowsNetworkUnavailable,
|
||
readOnlyFailureShowsNetworkUnavailable,
|
||
savedBridgeRequiresNetworkSetup,
|
||
isConnectionControlBootstrapSettling,
|
||
provisioningFailureRequiresFreshCandidate,
|
||
trustedConnectionBinding,
|
||
transportRefEquivalenceKey,
|
||
shouldRevealProvisioningNetworkStep,
|
||
retiredPhysicalReopenAuthority,
|
||
canIssueCanonicalStop,
|
||
physicalStopIntentCheckpoint,
|
||
authoritativeStateSupersedesPhysicalStopIntent,
|
||
connectionAttemptForRuntimeError,
|
||
recommendedConnectionRecoveryObservationTarget,
|
||
readOnlyConnectionObservationTarget,
|
||
} = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/lifecycle.ts",
|
||
));
|
||
({
|
||
K1ConnectionPipelines,
|
||
physicalRecoveryConnectionDetail,
|
||
shouldRenderK1OperationalPanels,
|
||
} = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/XgridsK1Connection.tsx",
|
||
));
|
||
({
|
||
K1OperatorError,
|
||
attemptNetworkPhaseLabel,
|
||
} = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/components/K1OperatorError.tsx",
|
||
));
|
||
({ hostFailureDiagnosticPresentation } = await server.ssrLoadModule(
|
||
"@xgrids-k1/frontend/hostDiagnosticPresentation.ts",
|
||
));
|
||
shellPresentation = await server.ssrLoadModule("/src/presentation.ts");
|
||
({ contourRuntimeAuthorityPresentation } = await server.ssrLoadModule(
|
||
"/src/workspaces/ContourHealthWorkspace.tsx",
|
||
));
|
||
});
|
||
|
||
after(async () => {
|
||
await server?.close();
|
||
});
|
||
|
||
function sourceSlice(source, startMarker, endMarker) {
|
||
const start = source.indexOf(startMarker);
|
||
const end = source.indexOf(endMarker, start + startMarker.length);
|
||
assert.notEqual(start, -1, `missing source marker: ${startMarker}`);
|
||
assert.notEqual(end, -1, `missing source marker: ${endMarker}`);
|
||
return source.slice(start, end);
|
||
}
|
||
|
||
test("K1 one-intent source contract makes mode reset explicit and keeps device I/O separate", () => {
|
||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||
const search = sourceSlice(source, "const repeatDeviceScan", "const submitConnect");
|
||
const apply = sourceSlice(source, "const submitConnect", "const verifyAppliedNetwork");
|
||
const mode = sourceSlice(source, "const changeDesiredConnectionMode", "const selectFreshDevice");
|
||
const selection = sourceSlice(
|
||
source,
|
||
"const selectFreshDevice",
|
||
"const chooseAnother",
|
||
);
|
||
|
||
assert.match(search, /scanWithResult\(\{[^}]*durationSeconds:\s*BLE_DISCOVERY_TIMEOUT_SECONDS/);
|
||
assert.equal((search.match(/scanWithResult\(/g) ?? []).length, 1);
|
||
assert.doesNotMatch(search, /\b(?:connect|verifyConnection|submitConnect)\s*\(/);
|
||
|
||
assert.match(mode, /await selectConnectionMode\(\{/);
|
||
assert.match(mode, /reset_scenario: true/);
|
||
assert.match(mode, /reset_id: resetId/);
|
||
assert.doesNotMatch(
|
||
mode,
|
||
/scanWithResult\(|verifyConnection\(|connect\(|prepareConnection/,
|
||
);
|
||
assert.doesNotMatch(
|
||
selection,
|
||
/await|scanWithResult\(|verifyConnection\(|connect\(|retireUnavailablePhysicalCommand|reopenRetiredPhysicalReconciliation/,
|
||
);
|
||
assert.match(selection, /requestExplicitProvisioning\(device\.device_id/);
|
||
assert.match(selection, /const actionableDevices = devices\.filter\(candidateSelectionAllowed\)/);
|
||
assert.doesNotMatch(selection, /physicallyRetired|reopenAuthority/);
|
||
const resultRows = sourceSlice(
|
||
source,
|
||
'<div className="device-list" aria-label="Результаты Bluetooth">',
|
||
'<div className="empty-state" role="status">',
|
||
);
|
||
assert.match(resultRows, /actionLabel="Выбрать"/);
|
||
assert.match(resultRows, /onSelect=\{\(\) => selectCandidate\(device\)\}/);
|
||
assert.doesNotMatch(resultRows, /Переподключиться|reopen|verifyConnection/);
|
||
assert.match(source, /onChange=\{\(event\) => setSsid\(event\.target\.value\)\}/);
|
||
assert.match(source, /onChange=\{\(event\) => setPassword\(event\.target\.value\)\}/);
|
||
|
||
assert.equal((apply.match(/await connect\(/g) ?? []).length, 1);
|
||
assert.doesNotMatch(
|
||
apply,
|
||
/scanWithResult\(|verifyConnection\(|retireUnavailablePhysicalCommand|reopenRetiredPhysicalReconciliation|candidateRefreshRequest|void submitConnect/,
|
||
);
|
||
assert.doesNotMatch(source, /allow_host_wifi_switch/);
|
||
assert.doesNotMatch(source, /<(?:button|input|select|textarea)\b/);
|
||
assert.doesNotMatch(
|
||
source,
|
||
/(?:color|background(?:-color)?):\s*(?:#[0-9a-f]{3,8}|rgba?\()/i,
|
||
);
|
||
for (const sharedControl of ["Button", "IconButton", "TextField", "ActivityIndicator", "StatusBadge"]) {
|
||
assert.match(source, new RegExp(`<${sharedControl}\\b`), sharedControl);
|
||
}
|
||
assert.equal((source.match(/buttonLabel:\s*"Применить"/g) ?? []).length, 3);
|
||
assert.match(source, /устарел|stale/i);
|
||
assert.match(source, /network_outcome_unknown|safe_to_retry/i);
|
||
});
|
||
|
||
test("exact network-applied REST proof completes frontend Apply before control is ready", () => {
|
||
const request = {
|
||
device_id: "BLE-K1-ONE",
|
||
connection_mode: "bridge",
|
||
idempotency_key: "apply-intent-one",
|
||
};
|
||
const operation = {
|
||
operation_id: "network-operation-one",
|
||
action: "network.provision",
|
||
status: "succeeded",
|
||
idempotency_key: request.idempotency_key,
|
||
context: { connection_mode: request.connection_mode },
|
||
result: { phase: "network_applied" },
|
||
};
|
||
const exactState = (controlState) => ({
|
||
connection_attempt: {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: operation.operation_id,
|
||
phase: "network_applied",
|
||
connection_mode: request.connection_mode,
|
||
control_state: controlState,
|
||
},
|
||
network_mutation_ledger: {
|
||
status: "resolved",
|
||
mutation_allowed: true,
|
||
operation_id: operation.operation_id,
|
||
intended_mode: request.connection_mode,
|
||
resolution: "target-observed",
|
||
transport_ref: request.device_id,
|
||
},
|
||
connection_supervisor: {
|
||
observed: {
|
||
device_network: {
|
||
state: "applied",
|
||
connection_mode: request.connection_mode,
|
||
transport_ref: request.device_id.toLowerCase(),
|
||
},
|
||
},
|
||
},
|
||
});
|
||
|
||
assert.equal(
|
||
exactAppliedNetworkIntentCompleted(
|
||
exactState("control_not_ready"),
|
||
request,
|
||
operation,
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
exactAppliedNetworkIntentCompleted(exactState("unknown"), request, operation),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
exactAppliedNetworkIntentCompleted({
|
||
...exactState("control_not_ready"),
|
||
connection_attempt: {
|
||
...exactState("control_not_ready").connection_attempt,
|
||
attempt_id: "another-operation",
|
||
},
|
||
}, request, operation),
|
||
false,
|
||
);
|
||
|
||
const runtime = readFileSync(new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
|
||
import.meta.url,
|
||
), "utf8");
|
||
const connectFlow = sourceSlice(
|
||
runtime,
|
||
"const connect = useCallback",
|
||
"const verifyConnection = useCallback",
|
||
);
|
||
assert.match(
|
||
connectFlow,
|
||
/const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted\([\s\S]*?if \(\s*!exactNetworkIntentCompleted\s*&& !hasExactConnectionReady\([\s\S]*?return requireExactConnectionReady\([\s\S]*?nextState = acceptSuccessfulConnectState\(nextState\)/,
|
||
);
|
||
assert.match(
|
||
connectFlow,
|
||
/observeProvisioningRequest\([\s\S]*?observedState = observation\.state[\s\S]*?const exactNetworkIntentCompleted = exactAppliedNetworkIntentCompleted\(/,
|
||
);
|
||
assert.doesNotMatch(
|
||
connectFlow,
|
||
/openApplicationControlSession|waitForControlPhase|verifyConnection\(|scanWithResult\(/,
|
||
);
|
||
});
|
||
|
||
function model() {
|
||
const activeModel = xgridsK1Manifest.spec.models[0];
|
||
assert.ok(activeModel);
|
||
return activeModel;
|
||
}
|
||
|
||
function supervisor({
|
||
control = true,
|
||
data = false,
|
||
dataPlaneState = "idle",
|
||
} = {}) {
|
||
const target = { ipv4: "192.168.68.52", port: 1883 };
|
||
return {
|
||
schema_version: "missioncore.k1-connection-supervisor/v1",
|
||
revision: 9,
|
||
closed: false,
|
||
intent: {
|
||
intent_id: "intent-001",
|
||
requested_mode: "bridge",
|
||
expected_device_id: "device-k1-001",
|
||
requested_at: OBSERVED_AT,
|
||
},
|
||
observed: {
|
||
device_network: {
|
||
state: "applied",
|
||
intent_id: "intent-001",
|
||
transport_ref: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
target,
|
||
source: "ble-read-only-status",
|
||
observed_at: OBSERVED_AT,
|
||
},
|
||
host_path: {
|
||
epoch: 5,
|
||
available: true,
|
||
fingerprint: "en0:192.168.68.10",
|
||
interface: "en0",
|
||
source_ipv4: "192.168.68.10",
|
||
route_class: "direct",
|
||
reason_code: null,
|
||
observed_at: OBSERVED_AT,
|
||
},
|
||
endpoint: {
|
||
target,
|
||
tcp_state: "reachable",
|
||
intent_id: "intent-001",
|
||
host_path_epoch: 5,
|
||
reason_code: null,
|
||
observed_at: OBSERVED_AT,
|
||
},
|
||
device_identity: {
|
||
state: control ? "verified" : "stale",
|
||
intent_id: "intent-001",
|
||
logical_device_id: "device-k1-001",
|
||
compatibility_profile_id: PROFILE_ID,
|
||
connection_mode: "bridge",
|
||
source: "mqtt-device-info",
|
||
host_path_epoch: 5,
|
||
observed_at: OBSERVED_AT,
|
||
},
|
||
control_plane: {
|
||
state: control ? "healthy" : "lost",
|
||
session_id: control ? "control-session-001" : null,
|
||
host_path_epoch: control ? 5 : null,
|
||
reason_code: control ? null : "control-heartbeat-lost",
|
||
observed_at: OBSERVED_AT,
|
||
},
|
||
data_plane: {
|
||
state: data ? "healthy" : dataPlaneState,
|
||
session_id: data || dataPlaneState !== "idle" ? "data-session-001" : null,
|
||
host_path_epoch: data || dataPlaneState !== "idle" ? 5 : null,
|
||
reason_code: dataPlaneState === "lost" ? "data-heartbeat-lost" : null,
|
||
observed_at: data || dataPlaneState !== "idle" ? OBSERVED_AT : null,
|
||
},
|
||
},
|
||
lease: {
|
||
state: "reachable",
|
||
generation: 6,
|
||
intent_id: "intent-001",
|
||
host_path_epoch: 5,
|
||
connection_mode: "bridge",
|
||
target,
|
||
logical_device_id: "device-k1-001",
|
||
reason_code: null,
|
||
observed_at: OBSERVED_AT,
|
||
},
|
||
authority: {
|
||
network_mutation_allowed: false,
|
||
control_allowed: control,
|
||
acquisition_start_allowed: control,
|
||
data_ingest_authoritative: control && data,
|
||
physical_motion_allowed: false,
|
||
reason_codes: control ? [] : ["control-heartbeat-lost"],
|
||
},
|
||
last_known: null,
|
||
allowed_actions: control ? ["stop-acquisition"] : ["probe-endpoint"],
|
||
};
|
||
}
|
||
|
||
function connectionLifecycle({
|
||
control = true,
|
||
mode = "bridge",
|
||
revision = 9,
|
||
} = {}) {
|
||
return {
|
||
schema_version: "missioncore.xgrids-k1-connection-lifecycle/v1",
|
||
revision,
|
||
desired_mode: mode,
|
||
configured_mode: mode,
|
||
active_mode: control ? mode : null,
|
||
mode_change: {
|
||
state: control ? "ready" : "awaiting-control",
|
||
from: mode,
|
||
to: mode,
|
||
},
|
||
mode_selection: {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
automatic_retry: false,
|
||
},
|
||
active_binding_key: control ? `binding-intent-001-${mode}` : null,
|
||
active_binding: control
|
||
? {
|
||
binding_key: `binding-intent-001-${mode}`,
|
||
intent_id: "intent-001",
|
||
transport_ref: "ble-k1-001",
|
||
connection_mode: mode,
|
||
target_ipv4: "192.168.68.52",
|
||
target_port: 1883,
|
||
host_path_epoch: 5,
|
||
control_session_id: "control-session-001",
|
||
logical_device_id: "device-k1-001",
|
||
compatibility_profile_id: PROFILE_ID,
|
||
}
|
||
: null,
|
||
connection_ready: control,
|
||
ready_to_start: control,
|
||
operation: null,
|
||
allowed_actions: control
|
||
? ["start-acquisition", "select-connection-mode"]
|
||
: ["select-connection-mode", "verify-control-read-only"],
|
||
automatic_retry: false,
|
||
};
|
||
}
|
||
|
||
function runtimeState() {
|
||
return {
|
||
phase: "live",
|
||
source_mode: "live",
|
||
ble_discovery_generation: 4,
|
||
connection_mode: "bridge",
|
||
configured_connection_mode: "bridge",
|
||
active_connection_mode: "bridge",
|
||
desired_connection_mode: "bridge",
|
||
desired_connection_mode_revision: 0,
|
||
k1_ip: "192.168.68.52",
|
||
rerun_grpc_url: "rerun+http://127.0.0.1:9877/proxy",
|
||
compatibility: {
|
||
profile_id: PROFILE_ID,
|
||
vendor_writes_enabled: true,
|
||
permitted_mode: "active-control",
|
||
},
|
||
device_ref: {
|
||
device_id: "device-k1-001",
|
||
model_id: model().id,
|
||
identity_stability: "stable",
|
||
identity_basis: "hardware-identifier",
|
||
},
|
||
device_session: {
|
||
device_session_id: "device-session-001",
|
||
device_id: "device-k1-001",
|
||
compatibility_profile_id: PROFILE_ID,
|
||
connectivity: "connected",
|
||
},
|
||
acquisition: {
|
||
acquisition_id: "acquisition-001",
|
||
device_id: "device-k1-001",
|
||
device_session_id: "device-session-001",
|
||
compatibility_profile_id: PROFILE_ID,
|
||
control_mode: "plugin-commanded",
|
||
requested_streams: ["spatial.point-cloud.live"],
|
||
target_host: "127.0.0.1",
|
||
duration_seconds: 0,
|
||
evidence_policy: "required",
|
||
state: "acquiring",
|
||
state_revision: 4,
|
||
},
|
||
sensor_catalog: {
|
||
schema_version: "missioncore.sensor-catalog/v1alpha2",
|
||
revision: "runtime-test",
|
||
streams: [{
|
||
stream_id: "spatial.point-cloud.live",
|
||
modality: "point-cloud",
|
||
availability: "streaming",
|
||
}],
|
||
},
|
||
metrics: {
|
||
pcl_frames: 10,
|
||
pose_frames: 5,
|
||
pipeline_ms: 12.5,
|
||
frame_rate_hz: 8,
|
||
point_count: 42_000,
|
||
ai_frame_rate_hz: 5,
|
||
device_elapsed_seconds: 30,
|
||
},
|
||
connection_supervisor: supervisor({ control: true, data: true }),
|
||
connection_lifecycle: connectionLifecycle(),
|
||
};
|
||
}
|
||
|
||
function activeRecoveryRuntimeState(recoveryOverrides = {}, stateOverrides = {}) {
|
||
const state = runtimeState();
|
||
return {
|
||
...state,
|
||
snapshot_runtime_id: "runtime-active-recovery-001",
|
||
snapshot_revision: 51,
|
||
producer_generation: 13,
|
||
phase: "reconnecting",
|
||
connection_supervisor: supervisor({
|
||
control: false,
|
||
data: false,
|
||
dataPlaneState: "lost",
|
||
}),
|
||
connection_lifecycle: connectionLifecycle({ control: false }),
|
||
connection_recovery: {
|
||
schema_version: "missioncore.xgrids-k1-active-stream-recovery/v1",
|
||
state: "reconnecting",
|
||
generation: 5,
|
||
acquisition_id: state.acquisition.acquisition_id,
|
||
attempt: 1,
|
||
started_at_utc: "2026-08-11T21:19:59Z",
|
||
elapsed_ms: 16_869,
|
||
reason_code: "host-route-unavailable",
|
||
force_finish_allowed: true,
|
||
automatic_read_only_rebind: true,
|
||
automatic_command_retry: false,
|
||
start_performed: false,
|
||
stop_performed: false,
|
||
ble_operation_performed: false,
|
||
network_mutation_performed: false,
|
||
runtime_producer_generation: 13,
|
||
camera_recovery: "owned",
|
||
camera_media_state: "pending-epoch",
|
||
camera_media_ready: false,
|
||
camera_epoch: null,
|
||
...recoveryOverrides,
|
||
},
|
||
...stateOverrides,
|
||
};
|
||
}
|
||
|
||
function normalize(state) {
|
||
return normalizeXgridsK1MissionState({ state }, model());
|
||
}
|
||
|
||
function durableTopologyState() {
|
||
return {
|
||
phase: "idle",
|
||
source_mode: "idle",
|
||
ble_discovery_generation: 0,
|
||
connection_mode: "bridge",
|
||
configured_connection_mode: "bridge",
|
||
active_connection_mode: null,
|
||
desired_connection_mode: "bridge",
|
||
desired_connection_mode_revision: 0,
|
||
devices: [],
|
||
operations: [],
|
||
network_write_reconciliation: null,
|
||
network_mutation_ledger: {
|
||
status: "empty",
|
||
mutation_allowed: true,
|
||
reason_code: null,
|
||
operation_id: null,
|
||
transport_ref: null,
|
||
intended_mode: null,
|
||
stage: null,
|
||
revision: null,
|
||
resolution: null,
|
||
updated_at_utc: null,
|
||
},
|
||
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: 3,
|
||
transport_ref: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
ipv4: "192.168.68.52",
|
||
compatibility_profile_id: PROFILE_ID,
|
||
firmware_version: "3.0.2",
|
||
source: "ble-read-only-status",
|
||
observed_at_utc: OBSERVED_AT,
|
||
},
|
||
},
|
||
};
|
||
}
|
||
|
||
function provisioningController(state) {
|
||
return {
|
||
state,
|
||
pendingAction: null,
|
||
scan: async () => undefined,
|
||
connect: async () => ({
|
||
succeeded: false,
|
||
networkIntentCompleted: false,
|
||
intentDisposition: "retain",
|
||
acceptedSessionKey: null,
|
||
}),
|
||
verifyConnection: async () => ({
|
||
succeeded: false,
|
||
reconciliationCompleted: false,
|
||
observedState: state,
|
||
}),
|
||
retireUnavailablePhysicalCommand: async () => false,
|
||
retireUnavailablePhysicalCommandWithResult: async () => ({
|
||
succeeded: false,
|
||
observedState: state,
|
||
}),
|
||
reopenRetiredPhysicalReconciliation: async () => ({
|
||
succeeded: false,
|
||
observedState: state,
|
||
}),
|
||
prepareConnectionReconfiguration: async () => false,
|
||
probeConfiguredEndpoint: async () => undefined,
|
||
};
|
||
}
|
||
|
||
function provisioningAttemptPresentation(overrides = {}) {
|
||
return {
|
||
snapshotRuntimeId: "runtime-network-action",
|
||
connectionMode: "bridge",
|
||
idempotencyKey: "apply-intent-one",
|
||
attemptId: null,
|
||
deviceId: "f89438fa-55ed-85ad-eed7-734ac84746d8",
|
||
label: "XGR-A46BE7",
|
||
rssi: -51,
|
||
ssid: "FIELD-NET",
|
||
localPhase: "submitting",
|
||
failureMessage: null,
|
||
freshStartAllowed: false,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
function terminalConnectionRecoveryState({
|
||
phase = "network_outcome_unknown",
|
||
safeNextAction = "verify-control-read-only",
|
||
includeConfiguredTarget = true,
|
||
includeScan = true,
|
||
} = {}) {
|
||
const state = durableTopologyState();
|
||
state.snapshot_runtime_id = "runtime-terminal-connection-recovery";
|
||
state.snapshot_runtime_started_at_utc = "2026-08-11T18:00:00Z";
|
||
state.snapshot_revision = 27;
|
||
state.connection_attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: "op-b7ea404d-b0a2-460a-80bd-6922e85a2df5",
|
||
connection_mode: "bridge",
|
||
status: "failed",
|
||
phase,
|
||
control_state: "unknown",
|
||
stage: "network-operation-failed",
|
||
public_error_code: null,
|
||
side_effect_status: phase === "network_outcome_unknown" ? "unknown" : "none",
|
||
safe_next_action: safeNextAction,
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: OBSERVED_AT,
|
||
timeline: [],
|
||
diagnostic_bundle: {
|
||
schema_version: "missioncore.xgrids-k1-connection-diagnostic/v1",
|
||
redacted: true,
|
||
generated_at_utc: OBSERVED_AT,
|
||
snapshot_runtime_id: "runtime-terminal-connection-recovery",
|
||
attempt: {},
|
||
network_mutation_ledger: {},
|
||
connection_supervisor: {},
|
||
automatic_retry: false,
|
||
},
|
||
};
|
||
const allowedActions = [];
|
||
const actions = {};
|
||
if (includeConfiguredTarget) {
|
||
allowedActions.push("observe-configured-device-network");
|
||
actions["observe-configured-device-network"] = {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: "F89438FA-55ED-85AD-EED7-734AC84746D8",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
};
|
||
}
|
||
if (includeScan) {
|
||
allowedActions.push("scan-ble");
|
||
actions["scan-ble"] = {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
};
|
||
}
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
recommended_action: includeConfiguredTarget
|
||
? "observe-configured-device-network"
|
||
: includeScan
|
||
? "scan-ble"
|
||
: "manual-recovery-required",
|
||
allowed_actions: allowedActions,
|
||
actions,
|
||
};
|
||
return state;
|
||
}
|
||
|
||
function reopenedPhysicalState({
|
||
stage = "observing",
|
||
advertisedRef = "f89438fa-55ed-85ad-eed7-734ac84746d8",
|
||
backendRef = "F89438FA-55ED-85AD-EED7-734AC84746D8",
|
||
reopeningId = "reopening-returned-k1",
|
||
discoveryGeneration = 13,
|
||
snapshotRevision = 20,
|
||
} = {}) {
|
||
const state = durableTopologyState();
|
||
state.snapshot_runtime_id = "runtime-reopened-k1";
|
||
state.snapshot_runtime_started_at_utc = "2026-08-10T12:00:00Z";
|
||
state.snapshot_revision = snapshotRevision;
|
||
state.desired_connection_mode = "bridge";
|
||
state.desired_connection_mode_revision = 7;
|
||
state.ble_discovery_generation = discoveryGeneration;
|
||
state.semantic_topology_store = {
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: null,
|
||
};
|
||
state.devices = [{
|
||
device_id: advertisedRef,
|
||
name: "XGR-A46BE7",
|
||
rssi: -61,
|
||
connectable: true,
|
||
likely_k1: true,
|
||
}];
|
||
state.physical_command = {
|
||
status: "unresolved",
|
||
reason_code: "physical-command-reconciliation-required",
|
||
requires_reconciliation: true,
|
||
automatic_replay_allowed: false,
|
||
normal_session_recovery_supported: false,
|
||
runtime_bound: false,
|
||
reconciliation_ready: true,
|
||
record: {
|
||
revision: 282,
|
||
operation_id: "old-stop-operation",
|
||
action: "stop",
|
||
stage,
|
||
resolution: null,
|
||
connection: { transport_ref: backendRef },
|
||
operator_retirements: [{
|
||
retirement_id: "retirement-old-stop",
|
||
retired_transport_ref: backendRef,
|
||
}],
|
||
operator_reconciliation_reopens: [{
|
||
reopening_id: reopeningId,
|
||
retirement_id: "retirement-old-stop",
|
||
retired_record_revision: 281,
|
||
reopened_transport_ref: advertisedRef,
|
||
discovery_generation: discoveryGeneration,
|
||
reason: "device-returned-for-explicit-reconciliation",
|
||
}],
|
||
},
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: {
|
||
retained_context_is_presence: false,
|
||
retired_transport_refs: [],
|
||
},
|
||
allowed_actions: ["observe-fresh-device-network", "scan-ble"],
|
||
actions: {
|
||
"observe-fresh-device-network": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: backendRef,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
"provision-fresh-device": {
|
||
allowed: false,
|
||
reason_codes: ["physical-command-reconciliation-required"],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: backendRef,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
return state;
|
||
}
|
||
|
||
function retiredPhysicalReopenReadyState({
|
||
advertisedRef = "f89438fa-55ed-85ad-eed7-734ac84746d8",
|
||
backendRef = "F89438FA-55ED-85AD-EED7-734AC84746D8",
|
||
discoveryGeneration = 13,
|
||
snapshotRuntimeId = "runtime-reopened-k1",
|
||
} = {}) {
|
||
const state = reopenedPhysicalState({
|
||
advertisedRef,
|
||
backendRef,
|
||
discoveryGeneration,
|
||
});
|
||
state.snapshot_runtime_id = snapshotRuntimeId;
|
||
state.physical_command = {
|
||
status: "resolved",
|
||
reason_code: null,
|
||
requires_reconciliation: false,
|
||
automatic_replay_allowed: false,
|
||
normal_session_recovery_supported: false,
|
||
runtime_bound: false,
|
||
reconciliation_ready: false,
|
||
operator_reconciliation_reopen: {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
expected_revision: 281,
|
||
expected_retirement_id: "retirement-old-stop",
|
||
expected_transport_ref: backendRef,
|
||
expected_discovery_generation: discoveryGeneration,
|
||
expected_desired_mode: "bridge",
|
||
expected_desired_mode_revision: 7,
|
||
device_io_performed: false,
|
||
automatic_retry: false,
|
||
},
|
||
record: {
|
||
revision: 281,
|
||
operation_id: "old-stop-operation",
|
||
action: "stop",
|
||
stage: "resolved",
|
||
resolution: "operator-retired-outcome-unknown",
|
||
connection: {
|
||
transport_ref: backendRef,
|
||
connection_mode: "bridge",
|
||
},
|
||
operator_retirements: [{
|
||
retirement_id: "retirement-old-stop",
|
||
retired_transport_ref: backendRef,
|
||
}],
|
||
operator_reconciliation_reopens: [],
|
||
},
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: {
|
||
retained_context_is_presence: false,
|
||
retired_transport_refs: [backendRef],
|
||
},
|
||
allowed_actions: ["scan-ble"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
"provision-fresh-device": {
|
||
allowed: false,
|
||
reason_codes: ["physical-command-target-retired"],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: backendRef,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
return state;
|
||
}
|
||
|
||
|
||
function reopenedPhysicalAuthority({ discoveryGeneration = 13 } = {}) {
|
||
return {
|
||
snapshotRuntimeId: "runtime-reopened-k1",
|
||
connectionMode: "bridge",
|
||
desiredModeRevision: 7,
|
||
reconfigurationRevision: 0,
|
||
reconfigurationIntentId: null,
|
||
activeBindingKey: null,
|
||
discoveryGeneration,
|
||
};
|
||
}
|
||
|
||
function connectionReconfiguration({
|
||
intent = null,
|
||
status = intent ? "awaiting-fresh-scan" : "idle",
|
||
revision = 0,
|
||
intentId = intent ? `reconfigure-${revision}` : null,
|
||
requiredTransportRef = null,
|
||
requiredConnectionMode = null,
|
||
discoveryGeneration = null,
|
||
requiredTransportObserved = null,
|
||
} = {}) {
|
||
return {
|
||
schema_version: "missioncore.xgrids-k1-connection-reconfiguration/v1",
|
||
revision,
|
||
intent_id: intentId,
|
||
intent,
|
||
status,
|
||
required_transport_ref: requiredTransportRef,
|
||
required_connection_mode: requiredConnectionMode,
|
||
minimum_discovery_generation: intent ? (discoveryGeneration ?? 1) : null,
|
||
fresh_discovery_generation: status === "fresh-scan-completed"
|
||
? discoveryGeneration
|
||
: null,
|
||
required_transport_observed: requiredTransportObserved,
|
||
prepared_at: intent ? OBSERVED_AT : null,
|
||
automatic_retry: false,
|
||
};
|
||
}
|
||
|
||
function acquisitionController(state) {
|
||
return {
|
||
state,
|
||
pendingAction: null,
|
||
error: null,
|
||
physicalStopIntentSpent: false,
|
||
physicalStopInFlight: false,
|
||
closeApplicationControlSession: async () => false,
|
||
prepareCanonicalAcquisition: async () => false,
|
||
startPreparedAcquisition: async () => false,
|
||
startReplay: async () => false,
|
||
stop: async () => false,
|
||
stopLocalReceiver: async () => false,
|
||
abort: async () => false,
|
||
};
|
||
}
|
||
|
||
function connectionPipelinesController(state) {
|
||
return {
|
||
...provisioningController(state),
|
||
...acquisitionController(state),
|
||
backendStatus: "offline",
|
||
eventStatus: "disconnected",
|
||
latencyHistory: [],
|
||
scanWithResult: async () => ({
|
||
observedState: state,
|
||
discoveryGeneration: state?.ble_discovery_generation ?? 0,
|
||
}),
|
||
prepareConnectionReconfigurationWithResult: async () => ({
|
||
succeeded: false,
|
||
observedState: state,
|
||
}),
|
||
getConnectionActionAuthority: () => null,
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
isConnectionActionAuthorityCurrent: () => true,
|
||
};
|
||
}
|
||
|
||
function renderConnectionPipelines(state, pendingAction = null) {
|
||
return renderToStaticMarkup(createElement(K1ConnectionPipelines, {
|
||
controller: {
|
||
...connectionPipelinesController(state),
|
||
pendingAction,
|
||
},
|
||
desiredConnectionMode: "bridge",
|
||
onDesiredConnectionModeChange() {},
|
||
operationalPanelsVisible: shouldRenderK1OperationalPanels(state, pendingAction),
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
sourceLabel: "Ожидание",
|
||
}));
|
||
}
|
||
|
||
function buttonMarkupWithText(markup, text) {
|
||
return (markup.match(/<button\b[\s\S]*?<\/button>/g) ?? [])
|
||
.filter((button) => button.includes(text));
|
||
}
|
||
|
||
function assertCanonicalConnectionCopy(markup) {
|
||
assert.doesNotMatch(
|
||
markup,
|
||
/[Ии]сходн|[Сс]охранён|[Пп]редыдущ|[Фф]изическ|[Пп]одключаемся/u,
|
||
);
|
||
}
|
||
|
||
function deviceRowOpeningTag(markup, deviceId) {
|
||
const deviceOffset = markup.indexOf(`<code>${deviceId}</code>`);
|
||
assert.notEqual(deviceOffset, -1, `${deviceId} must be rendered`);
|
||
const rowOffset = markup.lastIndexOf('<div class="device-row"', deviceOffset);
|
||
assert.notEqual(rowOffset, -1, `${deviceId} must belong to a device row`);
|
||
return markup.slice(rowOffset, markup.indexOf(">", rowOffset) + 1);
|
||
}
|
||
|
||
function deviceRowActionButton(markup, deviceId) {
|
||
const deviceOffset = markup.indexOf(`<code>${deviceId}</code>`);
|
||
assert.notEqual(deviceOffset, -1, `${deviceId} must be rendered`);
|
||
const buttonOffset = markup.indexOf("<button", deviceOffset);
|
||
assert.notEqual(buttonOffset, -1, `${deviceId} must have an action`);
|
||
const buttonEnd = markup.indexOf("</button>", buttonOffset);
|
||
assert.notEqual(buttonEnd, -1, `${deviceId} action must be complete`);
|
||
return markup.slice(buttonOffset, buttonEnd + "</button>".length);
|
||
}
|
||
|
||
function renderProvisioning(props) {
|
||
return renderToStaticMarkup(createElement(K1ProvisioningPipeline, props));
|
||
}
|
||
|
||
function captureProvisioningTree(props) {
|
||
let capturedTree = null;
|
||
|
||
function CaptureHarness() {
|
||
capturedTree = K1ProvisioningPipeline(props);
|
||
return capturedTree;
|
||
}
|
||
|
||
renderToStaticMarkup(createElement(CaptureHarness));
|
||
assert.ok(capturedTree);
|
||
return capturedTree;
|
||
}
|
||
|
||
function captureProvisioningTreeAfterSearch(
|
||
props,
|
||
{ completedDiscoveryGeneration } = {},
|
||
) {
|
||
let capturedTree = null;
|
||
|
||
function SearchCompletedCaptureHarness() {
|
||
const internals =
|
||
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
||
const dispatcher = internals?.H;
|
||
assert.equal(typeof dispatcher?.useState, "function");
|
||
const originalUseState = dispatcher.useState;
|
||
dispatcher.useState = (initialState) => {
|
||
if (initialState === emptySearchPresentation) {
|
||
const state = props.controller.state;
|
||
return [{
|
||
sequence: 1,
|
||
snapshotRuntimeId: state.snapshot_runtime_id ?? null,
|
||
connectionMode: props.desiredMode,
|
||
desiredModeRevision: state.desired_connection_mode_revision ?? null,
|
||
active: false,
|
||
completedDiscoveryGeneration: completedDiscoveryGeneration
|
||
?? state.ble_discovery_generation
|
||
?? null,
|
||
}, () => undefined];
|
||
}
|
||
return originalUseState(initialState);
|
||
};
|
||
try {
|
||
capturedTree = K1ProvisioningPipeline(props);
|
||
return capturedTree;
|
||
} finally {
|
||
dispatcher.useState = originalUseState;
|
||
}
|
||
}
|
||
|
||
renderToStaticMarkup(createElement(SearchCompletedCaptureHarness));
|
||
assert.ok(capturedTree);
|
||
return capturedTree;
|
||
}
|
||
|
||
function elementByProp(node, propName, expectedValue) {
|
||
if (Array.isArray(node)) {
|
||
for (const child of node) {
|
||
const found = elementByProp(child, propName, expectedValue);
|
||
if (found) return found;
|
||
}
|
||
return null;
|
||
}
|
||
if (!React.isValidElement(node)) return null;
|
||
if (node.props[propName] === expectedValue) return node;
|
||
return elementByProp(node.props.children, propName, expectedValue);
|
||
}
|
||
|
||
function createStatefulProvisioningHarness(initialProps) {
|
||
const hookSlots = [];
|
||
let currentProps = initialProps;
|
||
let capturedTree = null;
|
||
let pendingEffects = [];
|
||
|
||
const dependenciesMatch = (left, right) => Boolean(
|
||
left
|
||
&& right
|
||
&& left.length === right.length
|
||
&& left.every((value, index) => Object.is(value, right[index])),
|
||
);
|
||
|
||
const render = (nextProps = currentProps) => {
|
||
currentProps = nextProps;
|
||
pendingEffects = [];
|
||
|
||
function StatefulCaptureHarness() {
|
||
const internals =
|
||
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
||
const dispatcher = internals?.H;
|
||
assert.equal(typeof dispatcher?.useState, "function");
|
||
const originals = {
|
||
useState: dispatcher.useState,
|
||
useRef: dispatcher.useRef,
|
||
useMemo: dispatcher.useMemo,
|
||
useCallback: dispatcher.useCallback,
|
||
useEffect: dispatcher.useEffect,
|
||
};
|
||
let hookIndex = 0;
|
||
|
||
dispatcher.useState = (initialState) => {
|
||
const index = hookIndex;
|
||
hookIndex += 1;
|
||
if (!hookSlots[index]) {
|
||
hookSlots[index] = {
|
||
kind: "state",
|
||
initializer: initialState,
|
||
value: typeof initialState === "function"
|
||
? initialState()
|
||
: initialState,
|
||
};
|
||
}
|
||
const slot = hookSlots[index];
|
||
assert.equal(slot.kind, "state");
|
||
const setValue = (nextValue) => {
|
||
slot.value = typeof nextValue === "function"
|
||
? nextValue(slot.value)
|
||
: nextValue;
|
||
};
|
||
return [slot.value, setValue];
|
||
};
|
||
dispatcher.useRef = (initialValue) => {
|
||
const index = hookIndex;
|
||
hookIndex += 1;
|
||
if (!hookSlots[index]) {
|
||
hookSlots[index] = {
|
||
kind: "ref",
|
||
value: { current: initialValue },
|
||
};
|
||
}
|
||
const slot = hookSlots[index];
|
||
assert.equal(slot.kind, "ref");
|
||
return slot.value;
|
||
};
|
||
dispatcher.useMemo = (factory, dependencies) => {
|
||
const index = hookIndex;
|
||
hookIndex += 1;
|
||
const previous = hookSlots[index];
|
||
if (
|
||
!previous
|
||
|| previous.kind !== "memo"
|
||
|| !dependenciesMatch(previous.dependencies, dependencies)
|
||
) {
|
||
hookSlots[index] = {
|
||
kind: "memo",
|
||
dependencies,
|
||
value: factory(),
|
||
};
|
||
}
|
||
return hookSlots[index].value;
|
||
};
|
||
dispatcher.useCallback = (callback, dependencies) => {
|
||
const index = hookIndex;
|
||
hookIndex += 1;
|
||
const previous = hookSlots[index];
|
||
if (
|
||
!previous
|
||
|| previous.kind !== "callback"
|
||
|| !dependenciesMatch(previous.dependencies, dependencies)
|
||
) {
|
||
hookSlots[index] = {
|
||
kind: "callback",
|
||
dependencies,
|
||
value: callback,
|
||
};
|
||
}
|
||
return hookSlots[index].value;
|
||
};
|
||
dispatcher.useEffect = (effect, dependencies) => {
|
||
const index = hookIndex;
|
||
hookIndex += 1;
|
||
const previous = hookSlots[index];
|
||
const changed = !previous
|
||
|| previous.kind !== "effect"
|
||
|| !dependenciesMatch(previous.dependencies, dependencies);
|
||
hookSlots[index] = {
|
||
kind: "effect",
|
||
dependencies,
|
||
cleanup: previous?.kind === "effect" ? previous.cleanup : undefined,
|
||
};
|
||
if (changed) pendingEffects.push({ index, effect });
|
||
};
|
||
|
||
try {
|
||
capturedTree = K1ProvisioningPipeline(currentProps);
|
||
} finally {
|
||
dispatcher.useState = originals.useState;
|
||
dispatcher.useRef = originals.useRef;
|
||
dispatcher.useMemo = originals.useMemo;
|
||
dispatcher.useCallback = originals.useCallback;
|
||
dispatcher.useEffect = originals.useEffect;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
renderToStaticMarkup(createElement(StatefulCaptureHarness));
|
||
assert.ok(capturedTree);
|
||
return capturedTree;
|
||
};
|
||
|
||
const flushEffects = () => {
|
||
const effects = pendingEffects;
|
||
pendingEffects = [];
|
||
for (const { index, effect } of effects) {
|
||
const slot = hookSlots[index];
|
||
slot.cleanup?.();
|
||
const cleanup = effect();
|
||
slot.cleanup = typeof cleanup === "function" ? cleanup : undefined;
|
||
}
|
||
};
|
||
|
||
const stateValue = (initializer) => hookSlots.find(
|
||
(slot) => slot?.kind === "state" && slot.initializer === initializer,
|
||
)?.value;
|
||
|
||
const dispose = () => {
|
||
for (const slot of hookSlots) {
|
||
if (slot?.kind === "effect") slot.cleanup?.();
|
||
}
|
||
};
|
||
|
||
return { dispose, flushEffects, render, stateValue };
|
||
}
|
||
|
||
function reactNodeText(node) {
|
||
if (typeof node === "string" || typeof node === "number") return String(node);
|
||
if (Array.isArray(node)) return node.map(reactNodeText).join("");
|
||
if (!React.isValidElement(node)) return "";
|
||
return reactNodeText(node.props.children);
|
||
}
|
||
|
||
function actionByLabel(node, label) {
|
||
if (Array.isArray(node)) {
|
||
for (const child of node) {
|
||
const found = actionByLabel(child, label);
|
||
if (found) return found;
|
||
}
|
||
return null;
|
||
}
|
||
if (!React.isValidElement(node)) return null;
|
||
if (
|
||
typeof node.props.onClick === "function"
|
||
&& reactNodeText(node.props.children).includes(label)
|
||
) return node;
|
||
return actionByLabel(node.props.children, label);
|
||
}
|
||
|
||
function renderProvisioningWithAttempt(props, presentation, afterSearch = false) {
|
||
function AttemptHarness() {
|
||
const internals =
|
||
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
||
const dispatcher = internals?.H;
|
||
assert.equal(typeof dispatcher?.useState, "function");
|
||
const originalUseState = dispatcher.useState;
|
||
dispatcher.useState = (initialState) => {
|
||
if (afterSearch && initialState === emptySearchPresentation) {
|
||
const state = props.controller.state;
|
||
return [{ sequence: 1, snapshotRuntimeId: state.snapshot_runtime_id,
|
||
connectionMode: props.desiredMode, desiredModeRevision: state.desired_connection_mode_revision,
|
||
active: false, completedDiscoveryGeneration: state.ble_discovery_generation }, () => undefined];
|
||
}
|
||
if (initialState === emptyProvisioningAttemptPresentation) {
|
||
return [presentation, () => undefined];
|
||
}
|
||
return originalUseState(initialState);
|
||
};
|
||
try {
|
||
return K1ProvisioningPipeline(props);
|
||
} finally {
|
||
dispatcher.useState = originalUseState;
|
||
}
|
||
}
|
||
|
||
return renderToStaticMarkup(createElement(AttemptHarness));
|
||
}
|
||
|
||
function captureProvisioningTreeWithAttempt(props, presentation) {
|
||
let capturedTree = null;
|
||
|
||
function AttemptCaptureHarness() {
|
||
const internals =
|
||
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
||
const dispatcher = internals?.H;
|
||
assert.equal(typeof dispatcher?.useState, "function");
|
||
const originalUseState = dispatcher.useState;
|
||
dispatcher.useState = (initialState) => {
|
||
if (initialState === emptyProvisioningAttemptPresentation) {
|
||
return [presentation, () => undefined];
|
||
}
|
||
return originalUseState(initialState);
|
||
};
|
||
try {
|
||
capturedTree = K1ProvisioningPipeline(props);
|
||
return capturedTree;
|
||
} finally {
|
||
dispatcher.useState = originalUseState;
|
||
}
|
||
}
|
||
|
||
renderToStaticMarkup(createElement(AttemptCaptureHarness));
|
||
assert.ok(capturedTree);
|
||
return capturedTree;
|
||
}
|
||
|
||
function renderProvisioningAfterSearch(
|
||
props,
|
||
{ completedDiscoveryGeneration } = {},
|
||
) {
|
||
function SearchCompletedHarness() {
|
||
const internals =
|
||
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
||
const dispatcher = internals?.H;
|
||
assert.equal(typeof dispatcher?.useState, "function");
|
||
const originalUseState = dispatcher.useState;
|
||
dispatcher.useState = (initialState) => {
|
||
if (initialState === emptySearchPresentation) {
|
||
const state = props.controller.state;
|
||
return [{
|
||
sequence: 1,
|
||
snapshotRuntimeId: state.snapshot_runtime_id ?? null,
|
||
connectionMode: props.desiredMode,
|
||
desiredModeRevision: state.desired_connection_mode_revision ?? null,
|
||
active: false,
|
||
completedDiscoveryGeneration: completedDiscoveryGeneration
|
||
?? state.ble_discovery_generation
|
||
?? null,
|
||
}, () => undefined];
|
||
}
|
||
return originalUseState(initialState);
|
||
};
|
||
try {
|
||
return K1ProvisioningPipeline(props);
|
||
} finally {
|
||
dispatcher.useState = originalUseState;
|
||
}
|
||
}
|
||
|
||
return renderToStaticMarkup(createElement(SearchCompletedHarness));
|
||
}
|
||
|
||
|
||
function renderProvisioningWithCurrentPendingAction(
|
||
props,
|
||
{ publicSearch = true } = {},
|
||
) {
|
||
function CurrentPendingActionHarness() {
|
||
const state = props.controller.state;
|
||
const fence = {
|
||
snapshotRuntimeId: state.snapshot_runtime_id,
|
||
connectionMode: props.desiredMode,
|
||
desiredModeRevision: state.desired_connection_mode_revision,
|
||
reconfigurationRevision: state.connection_reconfiguration?.revision ?? 0,
|
||
reconfigurationIntentId: state.connection_reconfiguration?.intent_id ?? null,
|
||
activeBindingKey: state.connection_lifecycle?.active_binding_key ?? null,
|
||
discoveryGeneration: state.ble_discovery_generation,
|
||
clickToken: 1,
|
||
};
|
||
return createElement(
|
||
RuntimeActionFenceTestContext.Provider,
|
||
{ value: fence },
|
||
publicSearch
|
||
? createElement(SearchCompletedPendingHarness, { props })
|
||
: createElement(K1ProvisioningPipeline, props),
|
||
);
|
||
}
|
||
|
||
function SearchCompletedPendingHarness({ props: childProps }) {
|
||
const internals =
|
||
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
||
const dispatcher = internals?.H;
|
||
assert.equal(typeof dispatcher?.useState, "function");
|
||
const originalUseState = dispatcher.useState;
|
||
dispatcher.useState = (initialState) => {
|
||
if (initialState === emptySearchPresentation) {
|
||
const state = childProps.controller.state;
|
||
return [{
|
||
sequence: 1,
|
||
snapshotRuntimeId: state.snapshot_runtime_id ?? null,
|
||
connectionMode: childProps.desiredMode,
|
||
desiredModeRevision: state.desired_connection_mode_revision ?? null,
|
||
active: true,
|
||
completedDiscoveryGeneration: null,
|
||
}, () => undefined];
|
||
}
|
||
return originalUseState(initialState);
|
||
};
|
||
try {
|
||
return K1ProvisioningPipeline(childProps);
|
||
} finally {
|
||
dispatcher.useState = originalUseState;
|
||
}
|
||
}
|
||
|
||
return renderToStaticMarkup(createElement(CurrentPendingActionHarness));
|
||
}
|
||
|
||
|
||
function renderProvisioningWithRetainedDraft(
|
||
props,
|
||
{ device, draft, ssid, password },
|
||
) {
|
||
function RetainedDraftHarness() {
|
||
const internals =
|
||
React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
||
const dispatcher = internals?.H;
|
||
assert.equal(typeof dispatcher?.useState, "function");
|
||
const originalUseState = dispatcher.useState;
|
||
let emptyStringStateIndex = 0;
|
||
let nullStateIndex = 0;
|
||
dispatcher.useState = (initialState) => {
|
||
if (initialState === "") {
|
||
emptyStringStateIndex += 1;
|
||
const seededValue = emptyStringStateIndex === 1
|
||
? device.device_id
|
||
: emptyStringStateIndex === 2
|
||
? ssid
|
||
: emptyStringStateIndex === 3
|
||
? password
|
||
: initialState;
|
||
return [seededValue, () => undefined];
|
||
}
|
||
if (initialState === null) {
|
||
nullStateIndex += 1;
|
||
if (nullStateIndex === 1) return [device, () => undefined];
|
||
if (nullStateIndex === 8) return [draft, () => undefined];
|
||
}
|
||
return originalUseState(initialState);
|
||
};
|
||
try {
|
||
return K1ProvisioningPipeline(props);
|
||
} finally {
|
||
dispatcher.useState = originalUseState;
|
||
}
|
||
}
|
||
|
||
return renderToStaticMarkup(createElement(RetainedDraftHarness));
|
||
}
|
||
|
||
|
||
test("K1 provisioning password authority is scoped to one exact operator draft", () => {
|
||
const draft = {
|
||
snapshotRuntimeId: "runtime-a",
|
||
deviceId: "ble-k1-001",
|
||
connectionMode: "bridge",
|
||
desiredModeRevision: 7,
|
||
discoveryGeneration: 13,
|
||
reconfigurationRevision: 5,
|
||
reconfigurationIntentId: "reconfigure-001",
|
||
activeBindingKey: null,
|
||
requiredTransportRef: "ble-k1-001",
|
||
requiredConnectionMode: "bridge",
|
||
};
|
||
const current = {
|
||
snapshotRuntimeId: "runtime-a",
|
||
deviceId: "ble-k1-001",
|
||
connectionMode: "bridge",
|
||
desiredModeRevision: 7,
|
||
discoveryGeneration: 13,
|
||
reconfigurationRevision: 5,
|
||
reconfigurationIntentId: "reconfigure-001",
|
||
activeBindingKey: null,
|
||
requiredTransportRef: "ble-k1-001",
|
||
requiredConnectionMode: "bridge",
|
||
};
|
||
|
||
assert.equal(explicitProvisioningDraftMatches(draft, current), true);
|
||
for (const drifted of [
|
||
{ ...current, snapshotRuntimeId: "runtime-b" },
|
||
{ ...current, deviceId: "another-k1" },
|
||
{ ...current, connectionMode: "direct-connect" },
|
||
{ ...current, desiredModeRevision: 8 },
|
||
{ ...current, discoveryGeneration: 14 },
|
||
{ ...current, reconfigurationRevision: 6 },
|
||
{ ...current, reconfigurationIntentId: "reconfigure-002" },
|
||
{ ...current, activeBindingKey: "binding-new" },
|
||
{ ...current, requiredTransportRef: "ble-k1-002" },
|
||
{ ...current, requiredConnectionMode: "direct-connect" },
|
||
{ ...current, deviceId: null },
|
||
]) {
|
||
assert.equal(explicitProvisioningDraftMatches(draft, drifted), false);
|
||
}
|
||
});
|
||
|
||
test("K1 provisioning local credential fence changes with backend runtime authority", () => {
|
||
const fence = {
|
||
snapshotRuntimeId: "runtime-a",
|
||
reconfigurationRevision: 5,
|
||
reconfigurationIntentId: "reconfigure-001",
|
||
activeBindingKey: null,
|
||
requiredTransportRef: "ble-k1-001",
|
||
requiredConnectionMode: "bridge",
|
||
};
|
||
assert.notEqual(
|
||
localProvisioningDraftFenceKey(fence),
|
||
localProvisioningDraftFenceKey({
|
||
...fence,
|
||
snapshotRuntimeId: "runtime-b",
|
||
}),
|
||
);
|
||
});
|
||
|
||
test("K1 click-owned action requests disappear across runtime and click interleavings", () => {
|
||
const authorityA = {
|
||
snapshotRuntimeId: "runtime-a",
|
||
connectionMode: "bridge",
|
||
desiredModeRevision: 7,
|
||
reconfigurationRevision: 10,
|
||
reconfigurationIntentId: "reconfigure-a",
|
||
activeBindingKey: "binding-a",
|
||
discoveryGeneration: 5,
|
||
};
|
||
const firstClick = {
|
||
...authorityA,
|
||
clickToken: 17,
|
||
};
|
||
const awaitingScan = {
|
||
...firstClick,
|
||
deviceId: "ble-k1-001",
|
||
previousDiscoveryGeneration: 5,
|
||
expectedDiscoveryGeneration: null,
|
||
scanTransportRefs: [],
|
||
stage: "awaiting-scan",
|
||
};
|
||
let latestAuthority = authorityA;
|
||
const latestAuthorityMatches = (fence) =>
|
||
connectionActionAuthorityMatches(fence, latestAuthority);
|
||
|
||
assert.equal(
|
||
runtimeActionFenceMatches(
|
||
firstClick,
|
||
"runtime-a",
|
||
firstClick,
|
||
latestAuthorityMatches,
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
currentRuntimeActionRequest(
|
||
awaitingScan,
|
||
"runtime-a",
|
||
firstClick,
|
||
latestAuthorityMatches,
|
||
),
|
||
awaitingScan,
|
||
);
|
||
|
||
// Same-runtime authority drift must invalidate the old click before the new
|
||
// React render commits. A late Quick candidate cannot re-arm or auto-submit.
|
||
latestAuthority = { ...authorityA, discoveryGeneration: 6 };
|
||
assert.equal(
|
||
runtimeActionFenceMatches(
|
||
firstClick,
|
||
"runtime-a",
|
||
firstClick,
|
||
latestAuthorityMatches,
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
currentRuntimeActionRequest(
|
||
awaitingScan,
|
||
"runtime-a",
|
||
firstClick,
|
||
latestAuthorityMatches,
|
||
),
|
||
null,
|
||
);
|
||
|
||
// Simulate acceptState(runtime-b) updating latestState.current before React
|
||
// commits the runtime-b render.
|
||
latestAuthority = { ...authorityA, snapshotRuntimeId: "runtime-b" };
|
||
|
||
const secondClick = {
|
||
...latestAuthority,
|
||
clickToken: 18,
|
||
};
|
||
assert.equal(
|
||
runtimeActionFenceMatches(
|
||
firstClick,
|
||
"runtime-b",
|
||
secondClick,
|
||
latestAuthorityMatches,
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
runtimeActionFenceMatches(
|
||
secondClick,
|
||
"runtime-b",
|
||
secondClick,
|
||
latestAuthorityMatches,
|
||
),
|
||
true,
|
||
);
|
||
});
|
||
|
||
|
||
test("K1 reconfiguration continuation requires exact correlated +1 revisions", () => {
|
||
const fence = {
|
||
snapshotRuntimeId: "runtime-a",
|
||
connectionMode: "bridge",
|
||
desiredModeRevision: 7,
|
||
reconfigurationRevision: 10,
|
||
reconfigurationIntentId: null,
|
||
activeBindingKey: "binding-a",
|
||
discoveryGeneration: 5,
|
||
};
|
||
const preparedState = {
|
||
snapshot_runtime_id: "runtime-a",
|
||
desired_connection_mode: "bridge",
|
||
desired_connection_mode_revision: 7,
|
||
ble_discovery_generation: 6,
|
||
connection_reconfiguration: connectionReconfiguration({
|
||
intent: "select-device",
|
||
status: "awaiting-fresh-scan",
|
||
revision: 11,
|
||
intentId: "reconfigure-b",
|
||
discoveryGeneration: 6,
|
||
}),
|
||
connection_lifecycle: {
|
||
active_binding_key: null,
|
||
active_binding: null,
|
||
},
|
||
};
|
||
const preparedAuthority = {
|
||
...fence,
|
||
reconfigurationRevision: 11,
|
||
reconfigurationIntentId: "reconfigure-b",
|
||
activeBindingKey: null,
|
||
discoveryGeneration: 6,
|
||
};
|
||
assert.deepEqual(
|
||
reconfigurationContinuationAuthority(
|
||
fence,
|
||
preparedState,
|
||
"select-device",
|
||
preparedAuthority,
|
||
),
|
||
preparedAuthority,
|
||
);
|
||
|
||
for (const drifted of [
|
||
{ ...preparedAuthority, discoveryGeneration: 7 },
|
||
{ ...preparedAuthority, reconfigurationRevision: 12 },
|
||
{ ...preparedAuthority, desiredModeRevision: 8 },
|
||
{ ...preparedAuthority, activeBindingKey: "foreign-binding" },
|
||
]) {
|
||
assert.equal(
|
||
reconfigurationContinuationAuthority(
|
||
fence,
|
||
preparedState,
|
||
"select-device",
|
||
drifted,
|
||
),
|
||
null,
|
||
);
|
||
}
|
||
assert.equal(
|
||
reconfigurationContinuationAuthority(
|
||
fence,
|
||
preparedState,
|
||
"change-network",
|
||
preparedAuthority,
|
||
),
|
||
null,
|
||
);
|
||
const contaminatedPreparedState = {
|
||
...preparedState,
|
||
connection_lifecycle: {
|
||
active_binding_key: "binding-foreign",
|
||
active_binding: {
|
||
binding_key: "binding-foreign",
|
||
transport_ref: "ble-k1-foreign",
|
||
connection_mode: "bridge",
|
||
},
|
||
},
|
||
};
|
||
assert.equal(
|
||
reconfigurationContinuationAuthority(
|
||
fence,
|
||
contaminatedPreparedState,
|
||
"select-device",
|
||
{ ...preparedAuthority, activeBindingKey: "binding-foreign" },
|
||
),
|
||
null,
|
||
);
|
||
|
||
const cancelFence = preparedAuthority;
|
||
const cancelledState = {
|
||
...preparedState,
|
||
ble_discovery_generation: 7,
|
||
connection_reconfiguration: connectionReconfiguration({ revision: 12 }),
|
||
};
|
||
const cancelledAuthority = {
|
||
...cancelFence,
|
||
reconfigurationRevision: 12,
|
||
reconfigurationIntentId: null,
|
||
discoveryGeneration: 7,
|
||
};
|
||
assert.deepEqual(
|
||
reconfigurationContinuationAuthority(
|
||
cancelFence,
|
||
cancelledState,
|
||
"cancel",
|
||
cancelledAuthority,
|
||
),
|
||
cancelledAuthority,
|
||
);
|
||
});
|
||
|
||
test("separately explicit Verify rejects a foreign same-runtime binding", () => {
|
||
const exact = {
|
||
selected_device_id: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
connection_lifecycle: {
|
||
active_binding_key: "binding-a",
|
||
active_binding: {
|
||
binding_key: "binding-a",
|
||
transport_ref: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
},
|
||
},
|
||
};
|
||
assert.equal(
|
||
observedConnectionAuthorityAllowsTarget(
|
||
exact,
|
||
"ble-k1-001",
|
||
"bridge",
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
observedConnectionAuthorityAllowsTarget(
|
||
{
|
||
...exact,
|
||
selected_device_id: "ble-k1-foreign",
|
||
connection_lifecycle: {
|
||
active_binding_key: "binding-b",
|
||
active_binding: {
|
||
binding_key: "binding-b",
|
||
transport_ref: "ble-k1-foreign",
|
||
connection_mode: "bridge",
|
||
},
|
||
},
|
||
},
|
||
"ble-k1-001",
|
||
"bridge",
|
||
{ allowUnbound: true },
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
observedConnectionAuthorityAllowsTarget(
|
||
{
|
||
selected_device_id: null,
|
||
connection_mode: null,
|
||
connection_lifecycle: {
|
||
active_binding_key: null,
|
||
active_binding: null,
|
||
},
|
||
},
|
||
"ble-k1-001",
|
||
"bridge",
|
||
{ allowUnbound: true },
|
||
),
|
||
true,
|
||
);
|
||
});
|
||
|
||
test("K1 Connect continuation accepts only unchanged or exactly consumed authority", () => {
|
||
const ordinaryFence = {
|
||
snapshotRuntimeId: "runtime-a",
|
||
connectionMode: "bridge",
|
||
desiredModeRevision: 7,
|
||
reconfigurationRevision: 10,
|
||
reconfigurationIntentId: null,
|
||
activeBindingKey: null,
|
||
discoveryGeneration: 5,
|
||
};
|
||
const connectedState = ({
|
||
deviceId = "ble-k1-001",
|
||
reconfigurationRevision = 10,
|
||
discoveryGeneration = 5,
|
||
} = {}) => ({
|
||
snapshot_runtime_id: "runtime-a",
|
||
desired_connection_mode: "bridge",
|
||
desired_connection_mode_revision: 7,
|
||
ble_discovery_generation: discoveryGeneration,
|
||
selected_device_id: deviceId,
|
||
connection_mode: "bridge",
|
||
connection_reconfiguration: connectionReconfiguration({
|
||
revision: reconfigurationRevision,
|
||
}),
|
||
connection_lifecycle: {
|
||
active_binding_key: `binding-${deviceId}`,
|
||
active_binding: {
|
||
binding_key: `binding-${deviceId}`,
|
||
transport_ref: deviceId,
|
||
connection_mode: "bridge",
|
||
},
|
||
},
|
||
});
|
||
const ordinaryState = connectedState();
|
||
const ordinaryCurrent = {
|
||
...ordinaryFence,
|
||
activeBindingKey: "binding-ble-k1-001",
|
||
};
|
||
assert.deepEqual(
|
||
connectContinuationAuthority(
|
||
ordinaryFence,
|
||
ordinaryState,
|
||
ordinaryCurrent,
|
||
"ble-k1-001",
|
||
"bridge",
|
||
),
|
||
ordinaryCurrent,
|
||
);
|
||
|
||
const reconfigurationFence = {
|
||
...ordinaryFence,
|
||
reconfigurationIntentId: "reconfigure-a",
|
||
};
|
||
const consumedState = connectedState({
|
||
reconfigurationRevision: 11,
|
||
discoveryGeneration: 6,
|
||
});
|
||
const consumedCurrent = {
|
||
...reconfigurationFence,
|
||
reconfigurationRevision: 11,
|
||
reconfigurationIntentId: null,
|
||
activeBindingKey: "binding-ble-k1-001",
|
||
discoveryGeneration: 6,
|
||
};
|
||
assert.deepEqual(
|
||
connectContinuationAuthority(
|
||
reconfigurationFence,
|
||
consumedState,
|
||
consumedCurrent,
|
||
"ble-k1-001",
|
||
"bridge",
|
||
),
|
||
consumedCurrent,
|
||
);
|
||
|
||
const skippedState = connectedState({
|
||
reconfigurationRevision: 12,
|
||
discoveryGeneration: 7,
|
||
});
|
||
const skippedCurrent = {
|
||
...consumedCurrent,
|
||
reconfigurationRevision: 12,
|
||
discoveryGeneration: 7,
|
||
};
|
||
assert.equal(
|
||
connectContinuationAuthority(
|
||
reconfigurationFence,
|
||
skippedState,
|
||
skippedCurrent,
|
||
"ble-k1-001",
|
||
"bridge",
|
||
),
|
||
null,
|
||
);
|
||
const foreignState = connectedState({ deviceId: "ble-k1-foreign" });
|
||
assert.equal(
|
||
connectContinuationAuthority(
|
||
ordinaryFence,
|
||
foreignState,
|
||
{
|
||
...ordinaryCurrent,
|
||
activeBindingKey: "binding-ble-k1-foreign",
|
||
},
|
||
"ble-k1-001",
|
||
"bridge",
|
||
),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test("K1 runtime replacement retires A and isolates a new B action from late settlement", () => {
|
||
const arbiter = new SnapshotRuntimeActionArbiter();
|
||
const actionA = arbiter.begin(4);
|
||
assert.ok(actionA);
|
||
let pendingAction = "verify";
|
||
let acceptedState = "runtime-a";
|
||
let surfacedError = null;
|
||
|
||
assert.equal(
|
||
arbiter.retireForSnapshotChange("runtime-a", "runtime-b"),
|
||
true,
|
||
);
|
||
pendingAction = null;
|
||
acceptedState = "runtime-b";
|
||
assert.equal(pendingAction, null, "runtime-b controls are enabled without A loader");
|
||
|
||
const actionB = arbiter.begin(4);
|
||
assert.ok(actionB, "runtime-b click is admitted before action A settles");
|
||
pendingAction = "scan";
|
||
|
||
const acceptLateResult = (token, nextState) => {
|
||
if (!arbiter.isCurrent(token)) return false;
|
||
acceptedState = nextState;
|
||
return true;
|
||
};
|
||
const surfaceLateFailure = (token, message) => {
|
||
if (!arbiter.isCurrent(token)) return false;
|
||
surfacedError = message;
|
||
return true;
|
||
};
|
||
const finish = (token) => {
|
||
if (!arbiter.settle(token)) return false;
|
||
pendingAction = null;
|
||
return true;
|
||
};
|
||
|
||
assert.equal(acceptLateResult(actionA, "runtime-a-late"), false);
|
||
assert.equal(surfaceLateFailure(actionA, "late A error"), false);
|
||
assert.equal(finish(actionA), false);
|
||
assert.equal(acceptedState, "runtime-b");
|
||
assert.equal(surfacedError, null);
|
||
assert.equal(pendingAction, "scan", "late A finally cannot clear B pending state");
|
||
|
||
assert.equal(finish(actionB), true);
|
||
assert.equal(pendingAction, null);
|
||
});
|
||
|
||
test("successful action response may be superseded only by the same runtime revision", () => {
|
||
const response = {
|
||
snapshot_runtime_id: "runtime-a",
|
||
snapshot_revision: 41,
|
||
};
|
||
|
||
assert.equal(runtimeActionResponseAlreadyAccepted(response, {
|
||
snapshot_runtime_id: "runtime-a",
|
||
snapshot_revision: 42,
|
||
}), true);
|
||
assert.equal(runtimeActionResponseAlreadyAccepted(response, {
|
||
snapshot_runtime_id: "runtime-a",
|
||
snapshot_revision: 40,
|
||
}), false);
|
||
assert.equal(runtimeActionResponseAlreadyAccepted(response, {
|
||
snapshot_runtime_id: "runtime-b",
|
||
snapshot_revision: 99,
|
||
}), false);
|
||
assert.equal(runtimeActionResponseAlreadyAccepted(response, null), false);
|
||
});
|
||
|
||
test("explicit scenario reset B supersedes pending callback A in one runtime", () => {
|
||
const arbiter = new SnapshotRuntimeActionArbiter();
|
||
const actionA = arbiter.begin(12);
|
||
assert.ok(actionA);
|
||
|
||
const actionB = arbiter.begin(12, true);
|
||
assert.ok(actionB, "reset B is admitted while A is still pending");
|
||
assert.equal(arbiter.isCurrent(actionA), false);
|
||
assert.equal(arbiter.settle(actionA), false, "late A cannot clear B loader");
|
||
assert.equal(arbiter.isCurrent(actionB), true);
|
||
assert.equal(arbiter.settle(actionB), true);
|
||
});
|
||
|
||
test("acquisition and spatial STOP surfaces share one runtime action slot", () => {
|
||
const arbiter = new SnapshotRuntimeActionArbiter();
|
||
let physicalStopCallCount = 0;
|
||
const clickPhysicalStop = () => {
|
||
const token = arbiter.begin(7);
|
||
if (!token) return null;
|
||
physicalStopCallCount += 1;
|
||
return token;
|
||
};
|
||
|
||
const acquisitionSurface = clickPhysicalStop();
|
||
const spatialSurface = clickPhysicalStop();
|
||
assert.ok(acquisitionSurface);
|
||
assert.equal(spatialSurface, null);
|
||
assert.equal(physicalStopCallCount, 1);
|
||
assert.equal(arbiter.settle(acquisitionSurface), true);
|
||
});
|
||
|
||
test("change-network exact lookup never substitutes a neighboring UUID", () => {
|
||
const reconfiguration = connectionReconfiguration({
|
||
intent: "change-network",
|
||
status: "fresh-scan-completed",
|
||
revision: 8,
|
||
requiredTransportRef: "ble-k1-original",
|
||
requiredConnectionMode: "bridge",
|
||
discoveryGeneration: 11,
|
||
requiredTransportObserved: true,
|
||
});
|
||
const devices = [
|
||
{ device_id: "ble-k1-foreign", name: "Nearby", connectable: true },
|
||
{ device_id: "ble-k1-original", name: "Original", connectable: true },
|
||
];
|
||
assert.equal(
|
||
exactChangeNetworkCandidate(reconfiguration, devices, 11)?.device_id,
|
||
"ble-k1-original",
|
||
);
|
||
assert.equal(exactChangeNetworkCandidate(reconfiguration, devices, 12), null);
|
||
assert.equal(
|
||
exactChangeNetworkCandidate(
|
||
{ ...reconfiguration, required_transport_observed: false },
|
||
devices,
|
||
11,
|
||
),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test("cold connection workflow exposes the mode selector and explicit Scan immediately", () => {
|
||
const state = durableTopologyState();
|
||
state.semantic_topology_store = {
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: null,
|
||
};
|
||
state.devices = [{
|
||
device_id: "ble-k1-001",
|
||
name: "XGR-A46BE7",
|
||
rssi: -45,
|
||
connectable: true,
|
||
likely_k1: true,
|
||
}];
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["scan-ble", "provision-fresh-device"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
"provision-fresh-device": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
"prepare-select-device": {
|
||
allowed: false,
|
||
reason_codes: ["acquisition-active"],
|
||
target_source: "local-prestart-handoff",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
device_write_performed: false,
|
||
automatic_retry: false,
|
||
},
|
||
"prepare-change-network": {
|
||
allowed: false,
|
||
reason_codes: ["acquisition-active"],
|
||
target_source: "local-prestart-handoff",
|
||
required_transport_ref: "ble-k1-001",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: false,
|
||
device_write_performed: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
const markup = renderToStaticMarkup(createElement(K1ProvisioningPipeline, {
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
}));
|
||
assert.equal(physicalRecoveryConnectionDetail(state), null);
|
||
assert.match(markup, /ПОДКЛЮЧЕНИЕ · ШАГИ 01–02/);
|
||
assert.match(markup, /<span>01<\/span>/);
|
||
assert.match(markup, /<h3>Подключение<\/h3>/);
|
||
assert.doesNotMatch(markup, /<span>02<\/span>|<span>03<\/span>/);
|
||
assert.doesNotMatch(markup, /Питание|индикатор горит/);
|
||
assert.doesNotMatch(markup, /Название общей сети Wi‑Fi|Пароль Wi‑Fi/);
|
||
assert.doesNotMatch(markup, /class="nodedc-activity-indicator"/);
|
||
const scanButton = buttonMarkupWithText(markup, "Найти по Bluetooth")[0];
|
||
assert.ok(scanButton);
|
||
assert.doesNotMatch(scanButton, /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(
|
||
buttonMarkupWithText(markup, "Подключить новый K1").length,
|
||
0,
|
||
);
|
||
const modeToggle = markup.match(
|
||
/<button[^>]*aria-label="Способ подключения"[^>]*>/,
|
||
)?.[0];
|
||
assert.ok(modeToggle);
|
||
assert.doesNotMatch(modeToggle, /\bdisabled(?:=|\s|>)/);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
test("cold Scan click dispatches from the live runtime fence and releases it for retry", async () => {
|
||
const state = durableTopologyState();
|
||
state.snapshot_runtime_id = "runtime-live-scan-click";
|
||
state.snapshot_revision = 1;
|
||
state.semantic_topology_store = {
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: null,
|
||
};
|
||
state.connection_reconfiguration = connectionReconfiguration();
|
||
state.connection_lifecycle = connectionLifecycle({ control: false });
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["scan-ble"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
const authority = connectionActionAuthoritySnapshot(state, "bridge");
|
||
assert.ok(authority);
|
||
let scanCalls = 0;
|
||
let scanDispatched = null;
|
||
const controller = {
|
||
...provisioningController(state),
|
||
scanWithResult: async () => {
|
||
scanCalls += 1;
|
||
scanDispatched?.();
|
||
return {
|
||
succeeded: true,
|
||
snapshotRuntimeId: state.snapshot_runtime_id,
|
||
discoveryGeneration: state.ble_discovery_generation + scanCalls,
|
||
transportRefs: [],
|
||
};
|
||
},
|
||
selectConnectionMode: async () => true,
|
||
getConnectionActionAuthority: () => authority,
|
||
isSnapshotRuntimeCurrent: (runtimeId) => runtimeId === state.snapshot_runtime_id,
|
||
isConnectionActionAuthorityCurrent: (candidate) =>
|
||
connectionActionAuthorityMatches(candidate, authority),
|
||
isConnectionPolicyActionAllowedCurrent: (action) => action === "scan-ble",
|
||
};
|
||
const tree = captureProvisioningTree({
|
||
controller,
|
||
desiredMode: "bridge",
|
||
});
|
||
const scanButton = actionByLabel(tree, "Найти по Bluetooth");
|
||
assert.ok(scanButton);
|
||
|
||
const clickAndWaitForDispatch = async () => {
|
||
const dispatched = new Promise((resolve) => {
|
||
scanDispatched = resolve;
|
||
});
|
||
scanButton.props.onClick();
|
||
await Promise.race([
|
||
dispatched,
|
||
new Promise((_, reject) => setTimeout(
|
||
() => reject(new Error("Scan click did not reach scanWithResult")),
|
||
100,
|
||
)),
|
||
]);
|
||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||
};
|
||
|
||
await clickAndWaitForDispatch();
|
||
await clickAndWaitForDispatch();
|
||
assert.equal(scanCalls, 2);
|
||
});
|
||
|
||
test("scenario reset epoch rejects every late settlement from the old local scenario", () => {
|
||
assert.equal(localScenarioActionEpochIsCurrent(4, 4), true);
|
||
assert.equal(localScenarioActionEpochIsCurrent(4, 5), false);
|
||
|
||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||
const search = sourceSlice(source, "const repeatDeviceScan", "const submitConnect");
|
||
const afterScan = search.indexOf("scanResult = await scanWithResult");
|
||
const epochGuard = search.indexOf(
|
||
"localScenarioActionEpochIsCurrent",
|
||
afterScan,
|
||
);
|
||
const firstSettlement = search.indexOf("setEscapedAppliedAttemptKey", afterScan);
|
||
assert.ok(afterScan >= 0);
|
||
assert.ok(epochGuard > afterScan);
|
||
assert.ok(firstSettlement > epochGuard);
|
||
|
||
const toolbarResetBoundary = sourceSlice(
|
||
source,
|
||
"if (\n scenarioResetPresentationKey",
|
||
"const currentRuntimeActionFence",
|
||
);
|
||
assert.match(toolbarResetBoundary, /localScenarioActionEpoch\.current \+= 1/);
|
||
assert.match(toolbarResetBoundary, /activeRuntimeActionFence\.current = null/);
|
||
assert.match(toolbarResetBoundary, /activeConnectIntent\.current = null/);
|
||
|
||
const resetEffect = sourceSlice(
|
||
source,
|
||
"useEffect(() => {\n if (\n !scenarioResetPresentationKey",
|
||
"const searchPresentationIsCurrent",
|
||
);
|
||
assert.match(resetEffect, /setConnectionAttemptPresentation\(null\)/);
|
||
assert.match(resetEffect, /setPreparingReconfigurationRequest\(null\)/);
|
||
assert.match(resetEffect, /setCandidateUnavailableMessage\(null\)/);
|
||
assert.doesNotMatch(source, /physicalReopenInFlight|setPhysicalReopenPresentation/);
|
||
});
|
||
|
||
test("toolbar reset in one runtime rejects late Scan A and admits exactly one Scan B", async () => {
|
||
const initialState = durableTopologyState();
|
||
initialState.snapshot_runtime_id = "runtime-toolbar-reset-during-scan";
|
||
initialState.snapshot_revision = 1;
|
||
initialState.semantic_topology_store = {
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: null,
|
||
};
|
||
initialState.connection_reconfiguration = connectionReconfiguration();
|
||
initialState.connection_lifecycle = connectionLifecycle({ control: false });
|
||
initialState.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["scan-ble"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
let currentState = initialState;
|
||
let resolveScanA;
|
||
let markScanADispatched;
|
||
let markScanBDispatched;
|
||
const scanAResult = new Promise((resolve) => {
|
||
resolveScanA = resolve;
|
||
});
|
||
const scanADispatched = new Promise((resolve) => {
|
||
markScanADispatched = resolve;
|
||
});
|
||
const scanBDispatched = new Promise((resolve) => {
|
||
markScanBDispatched = resolve;
|
||
});
|
||
let scanCalls = 0;
|
||
let scanBCalls = 0;
|
||
|
||
const controller = {
|
||
...provisioningController(initialState),
|
||
selectConnectionMode: async () => true,
|
||
scanWithResult: async () => {
|
||
scanCalls += 1;
|
||
if (scanCalls === 1) {
|
||
markScanADispatched();
|
||
return scanAResult;
|
||
}
|
||
scanBCalls += 1;
|
||
markScanBDispatched();
|
||
return {
|
||
succeeded: true,
|
||
snapshotRuntimeId: currentState.snapshot_runtime_id,
|
||
discoveryGeneration: currentState.ble_discovery_generation + 1,
|
||
transportRefs: [],
|
||
};
|
||
},
|
||
getCurrentState: () => currentState,
|
||
getConnectionActionAuthority: (mode) =>
|
||
connectionActionAuthoritySnapshot(currentState, mode),
|
||
isSnapshotRuntimeCurrent: (runtimeId) =>
|
||
runtimeId === currentState.snapshot_runtime_id,
|
||
isConnectionActionAuthorityCurrent: (candidate) =>
|
||
connectionActionAuthorityMatches(
|
||
candidate,
|
||
connectionActionAuthoritySnapshot(
|
||
currentState,
|
||
candidate.connectionMode,
|
||
),
|
||
),
|
||
isConnectionPolicyActionAllowedCurrent: (action) =>
|
||
currentState.connection_policy.allowed_actions.includes(action),
|
||
};
|
||
const props = () => ({
|
||
controller: { ...controller, state: currentState },
|
||
desiredMode: "bridge",
|
||
});
|
||
const harness = createStatefulProvisioningHarness(props());
|
||
|
||
try {
|
||
let tree = harness.render(props());
|
||
harness.flushEffects();
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
const scanAButton = actionByLabel(tree, "Найти по Bluetooth");
|
||
assert.ok(scanAButton);
|
||
scanAButton.props.onClick();
|
||
await Promise.race([
|
||
scanADispatched,
|
||
new Promise((_, reject) => setTimeout(
|
||
() => reject(new Error("Scan A did not dispatch")),
|
||
100,
|
||
)),
|
||
]);
|
||
|
||
const resetState = structuredClone(initialState);
|
||
resetState.snapshot_revision = 2;
|
||
resetState.desired_connection_mode_revision = 1;
|
||
resetState.connection_scenario_reset = {
|
||
reset_id: "toolbar-reset-same-runtime",
|
||
request_revision: 0,
|
||
revision: 1,
|
||
desired_mode: "bridge",
|
||
active: false,
|
||
settled_by_discovery_generation: 0,
|
||
local_session_closed: true,
|
||
previous_device_may_continue_scanning: false,
|
||
physical_disposition: null,
|
||
network_disposition: null,
|
||
device_command_performed: false,
|
||
network_write_performed: false,
|
||
automatic_scan: false,
|
||
operation_sequence: 2,
|
||
};
|
||
currentState = resetState;
|
||
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
assert.equal(harness.stateValue(emptySearchPresentation), null);
|
||
assert.ok(actionByLabel(tree, "Найти по Bluetooth"));
|
||
assert.equal(actionByLabel(tree, "Повторить поиск Bluetooth"), null);
|
||
assert.equal(actionByLabel(tree, "Применить"), null);
|
||
|
||
resolveScanA({
|
||
succeeded: true,
|
||
snapshotRuntimeId: initialState.snapshot_runtime_id,
|
||
discoveryGeneration: 1,
|
||
transportRefs: ["stale-device-from-scan-a"],
|
||
});
|
||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
assert.equal(
|
||
harness.stateValue(emptySearchPresentation),
|
||
null,
|
||
"late Scan A must not repopulate the reset search latch",
|
||
);
|
||
assert.ok(actionByLabel(tree, "Найти по Bluetooth"));
|
||
assert.equal(actionByLabel(tree, "Повторить поиск Bluetooth"), null);
|
||
assert.equal(actionByLabel(tree, "Применить"), null);
|
||
|
||
const scanBButton = actionByLabel(tree, "Найти по Bluetooth");
|
||
scanBButton.props.onClick();
|
||
await Promise.race([
|
||
scanBDispatched,
|
||
new Promise((_, reject) => setTimeout(
|
||
() => reject(new Error("Scan B did not dispatch")),
|
||
100,
|
||
)),
|
||
]);
|
||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||
assert.equal(scanCalls, 2);
|
||
assert.equal(scanBCalls, 1);
|
||
} finally {
|
||
harness.dispose();
|
||
}
|
||
});
|
||
|
||
test("operator-local connection belongs to LAB and retains all test modes", () => {
|
||
const localConnection = workspaces.find((workspace) => workspace.id === "local-device");
|
||
assert.equal(localConnection?.root, "polygon");
|
||
assert.equal(localConnection?.label, "Тестовые устройства");
|
||
assert.equal(localConnection?.title, "Тестовые устройства");
|
||
assert.equal(model().displayName, "XGRIDS LixelKity K1");
|
||
assert.deepEqual(
|
||
connectionModeOptions.map(({ label }) => label),
|
||
[
|
||
"Общая сеть · Bridge",
|
||
"Локальная сеть · Quick Connect",
|
||
"Хотспот контроллера · Direct Connect",
|
||
],
|
||
);
|
||
assertCanonicalConnectionCopy(
|
||
connectionModeOptions
|
||
.map(({ label, description }) => `${label} ${description ?? ""}`)
|
||
.join(" "),
|
||
);
|
||
});
|
||
|
||
test("cold adjacent connection panels do not repeat the model or device noun", () => {
|
||
const state = durableTopologyState();
|
||
state.semantic_topology_store = {
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: null,
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["scan-ble", "provision-fresh-device"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
const provisioningMarkup = renderToStaticMarkup(createElement(
|
||
K1ProvisioningPipeline,
|
||
{ controller: provisioningController(state), desiredMode: "bridge" },
|
||
));
|
||
const acquisitionMarkup = renderToStaticMarkup(createElement(
|
||
K1AcquisitionPipeline,
|
||
{
|
||
controller: acquisitionController(state),
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
},
|
||
));
|
||
const diagnosticsMarkup = renderToStaticMarkup(createElement(
|
||
K1Diagnostics,
|
||
{
|
||
controller: {
|
||
state,
|
||
backendStatus: "offline",
|
||
eventStatus: "disconnected",
|
||
latencyHistory: [],
|
||
},
|
||
sourceLabel: "Ожидание",
|
||
},
|
||
));
|
||
assertCanonicalConnectionCopy(
|
||
`${provisioningMarkup}${acquisitionMarkup}${diagnosticsMarkup}`,
|
||
);
|
||
});
|
||
|
||
test("legacy live fields do not create device, stream or metrics authority", () => {
|
||
const state = runtimeState();
|
||
delete state.connection_supervisor;
|
||
|
||
const normalized = normalize(state);
|
||
assert.ok(normalized);
|
||
assert.equal(normalized.activeDevice, null);
|
||
assert.equal(normalized.deviceSession.connectivity, "unknown");
|
||
assert.equal(normalized.sourceMode, "idle");
|
||
assert.equal(normalized.phase, "starting");
|
||
assert.equal(normalized.spatialSource, null);
|
||
assert.equal(normalized.metrics.latencyMs, null);
|
||
assert.equal(normalized.metrics.frameRateHz, null);
|
||
assert.equal(normalized.metrics.aiFrameRateHz, null);
|
||
assert.equal(normalized.observationSources[0].availability, "unverified");
|
||
assert.equal(normalized.observationSources[0].binding.deviceId, null);
|
||
});
|
||
|
||
test("control authority exposes the session but not unconfirmed data", () => {
|
||
const state = runtimeState();
|
||
state.connection_supervisor = supervisor({ control: true, data: false });
|
||
|
||
const normalized = normalize(state);
|
||
assert.ok(normalized?.activeDevice);
|
||
assert.equal(normalized.activeDevice.endpointLabel, "192.168.68.52");
|
||
assert.equal(normalized.deviceSession.connectivity, "connected");
|
||
assert.equal(normalized.sourceMode, "idle");
|
||
assert.equal(normalized.phase, "starting");
|
||
assert.equal(normalized.spatialSource, null);
|
||
assert.equal(normalized.metrics.pointCount, null);
|
||
assert.equal(normalized.observationSources[0].availability, "available");
|
||
assert.equal(normalized.observationSources[0].previewUrl, null);
|
||
});
|
||
|
||
test("control plus data authority is the only live streaming presentation", () => {
|
||
const normalized = normalize(runtimeState());
|
||
assert.ok(normalized?.activeDevice);
|
||
assert.equal(normalized.sourceMode, "live");
|
||
assert.equal(normalized.phase, "streaming");
|
||
assert.equal(normalized.deviceSession.connectivity, "connected");
|
||
assert.equal(normalized.spatialSource.url, "rerun+http://127.0.0.1:9877/proxy");
|
||
assert.equal(normalized.metrics.publishedFrameCount, 15);
|
||
assert.equal(normalized.metrics.latencyMs, 12.5);
|
||
assert.equal(normalized.metrics.aiFrameRateHz, 5);
|
||
assert.equal(normalized.observationSources[0].availability, "streaming");
|
||
});
|
||
|
||
test("successful K1 scanning state stays a success message", () => {
|
||
const state = runtimeState();
|
||
state.message = "K1 подтвердил режим сканирования; приём и запись активны.";
|
||
|
||
const normalized = normalize(state);
|
||
assert.equal(normalized.message, state.message);
|
||
assert.notEqual(
|
||
normalized.message,
|
||
"Операция не выполнена. Технические подробности сохранены в журнале сервера.",
|
||
);
|
||
});
|
||
|
||
test("data loss degrades the controlled device and withdraws stale stream data", () => {
|
||
const state = runtimeState();
|
||
state.connection_supervisor = supervisor({
|
||
control: true,
|
||
data: false,
|
||
dataPlaneState: "lost",
|
||
});
|
||
|
||
const normalized = normalize(state);
|
||
assert.ok(normalized?.activeDevice);
|
||
assert.equal(normalized.deviceSession.connectivity, "degraded");
|
||
assert.equal(normalized.sourceMode, "idle");
|
||
assert.equal(normalized.phase, "starting");
|
||
assert.equal(normalized.spatialSource, null);
|
||
assert.equal(normalized.metrics.frameRateHz, null);
|
||
assert.equal(normalized.observationSources[0].availability, "degraded");
|
||
assert.equal(normalized.observationSources[0].previewUrl, null);
|
||
});
|
||
|
||
test("exact reconnecting lineage retains only the current spatial presentation lease", () => {
|
||
const healthy = normalize(runtimeState());
|
||
const recoveryState = activeRecoveryRuntimeState();
|
||
const recovering = normalize(recoveryState);
|
||
assert.ok(healthy?.spatialSource && recovering?.spatialSource);
|
||
assert.equal(recovering.sourceMode, "live");
|
||
assert.equal(recovering.phase, "starting");
|
||
assert.equal(recovering.spatialSource.id, healthy.spatialSource.id);
|
||
assert.equal(recovering.spatialSource.url, healthy.spatialSource.url);
|
||
assert.equal(recovering.activeDevice, null);
|
||
assert.equal(recovering.metrics.publishedFrameCount, null);
|
||
assert.equal(recovering.metrics.latencyMs, null);
|
||
assert.equal(recovering.observationSources[0].availability, "connecting");
|
||
assert.equal(recovering.observationSources[0].previewUrl, healthy.spatialSource.url);
|
||
assert.deepEqual(recovering.observationSources[0].presentationLease, {
|
||
kind: "active-stream-recovery",
|
||
runtimeId: "runtime-active-recovery-001",
|
||
acquisitionId: "acquisition-001",
|
||
acquisitionStateRevision: 4,
|
||
producerGeneration: 13,
|
||
recoveryGeneration: 5,
|
||
});
|
||
|
||
const noCurrentUrl = structuredClone(recoveryState);
|
||
delete noCurrentUrl.rerun_grpc_url;
|
||
assert.equal(normalize(noCurrentUrl).spatialSource, null);
|
||
assert.equal(normalize(noCurrentUrl).observationSources[0].previewUrl, null);
|
||
});
|
||
|
||
test("stale or terminal recovery lineage cannot retain a spatial source", () => {
|
||
const staleProducer = activeRecoveryRuntimeState();
|
||
staleProducer.producer_generation += 1;
|
||
|
||
const differentAcquisition = activeRecoveryRuntimeState({
|
||
acquisition_id: "acquisition-from-old-runtime",
|
||
});
|
||
|
||
for (const state of [
|
||
staleProducer,
|
||
differentAcquisition,
|
||
activeRecoveryRuntimeState({ state: "blocked" }),
|
||
activeRecoveryRuntimeState({ state: "standby" }),
|
||
activeRecoveryRuntimeState({ state: "fault" }),
|
||
]) {
|
||
const normalized = normalize(state);
|
||
assert.equal(normalized.sourceMode, "idle");
|
||
assert.equal(normalized.spatialSource, null);
|
||
assert.equal(normalized.metrics.publishedFrameCount, null);
|
||
assert.equal(normalized.observationSources[0].previewUrl, null);
|
||
assert.equal(normalized.observationSources[0].presentationLease, null);
|
||
}
|
||
});
|
||
|
||
test("control loss turns a selected device back into an unverified snapshot", () => {
|
||
const state = runtimeState();
|
||
state.connection_supervisor = supervisor({ control: false, data: false });
|
||
|
||
const normalized = normalize(state);
|
||
assert.ok(normalized);
|
||
assert.equal(normalized.activeDevice, null);
|
||
assert.equal(normalized.deviceSession.connectivity, "connecting");
|
||
assert.equal(normalized.sourceMode, "idle");
|
||
assert.equal(normalized.spatialSource, null);
|
||
assert.equal(normalized.observationSources[0].availability, "unverified");
|
||
assert.equal(normalized.observationSources[0].binding.deviceId, null);
|
||
});
|
||
|
||
test("operator banner renders all safe host diagnostics and never the raw exception", () => {
|
||
const diagnostic = {
|
||
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,
|
||
};
|
||
const presentation = hostFailureDiagnosticPresentation(diagnostic);
|
||
assert.ok(presentation);
|
||
assert.equal(
|
||
presentation.operatorActionLabel,
|
||
"Разблокируйте связку ключей macOS и подтвердите доступ Mission Core к профилю подключения.",
|
||
);
|
||
|
||
const privateException = "SecurityError: account=private-user password=never-render-this";
|
||
const attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: "op-00000000-0000-4000-8000-000000000123",
|
||
connection_mode: "bridge",
|
||
status: "failed",
|
||
stage: "status-observing-failed",
|
||
public_error_code: "network-session-interrupted",
|
||
side_effect_status: "unknown",
|
||
safe_next_action: "scan-select-connect",
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: OBSERVED_AT,
|
||
timeline: [],
|
||
diagnostic_bundle: {
|
||
schema_version: "missioncore.xgrids-k1-connection-diagnostic/v1",
|
||
redacted: true,
|
||
generated_at_utc: OBSERVED_AT,
|
||
snapshot_runtime_id: "runtime-test",
|
||
attempt: {},
|
||
network_mutation_ledger: {},
|
||
connection_supervisor: {},
|
||
automatic_retry: false,
|
||
},
|
||
};
|
||
const markup = renderToStaticMarkup(createElement(K1OperatorError, {
|
||
message: privateException,
|
||
diagnostic,
|
||
attempt,
|
||
onRefresh() {},
|
||
onClear() {},
|
||
}));
|
||
|
||
assert.match(markup, /Причина/);
|
||
assert.match(markup, /Связка ключей требует явного подтверждения оператора/);
|
||
assert.match(markup, /Системный контур/);
|
||
assert.match(markup, /Связка ключей macOS/);
|
||
assert.match(markup, /Управляющая связь не установлена/);
|
||
assert.match(markup, /Разблокируйте связку ключей macOS/);
|
||
assert.match(markup, /Автоматического повтора не было/);
|
||
assert.match(markup, /op-00000000-0000-4000-8000-000000000123/);
|
||
assert.match(markup, /Ожидание ответа/);
|
||
assert.match(markup, /Результат команды не подтверждён/);
|
||
assert.match(markup, /новый поиск и выбрать результат/);
|
||
assert.match(markup, /Скопировать диагностику/);
|
||
assert.match(markup, /<details class="error-banner__details">/);
|
||
assert.doesNotMatch(markup, /<details[^>]*\sopen/);
|
||
assert.match(markup, /Проверить состояние/);
|
||
assert.doesNotMatch(markup, /SecurityError|private-user|never-render-this/);
|
||
assert.doesNotMatch(markup, /host\.keychain\.interaction-required/);
|
||
});
|
||
|
||
test("operator banner presents every canonical connection next action without fallback drift", () => {
|
||
const labels = new Map([
|
||
["wait-for-current-attempt", "Дождаться завершения текущей попытки"],
|
||
["continue-with-control-verification", "Продолжить текущее подключение"],
|
||
["verify-control-read-only", "Проверить управление без изменения сети"],
|
||
["start-acquisition", "Готово к запуску приёма"],
|
||
["stop-local-receiver", "Завершить только локальный приём"],
|
||
[
|
||
"retire-unavailable-physical-target",
|
||
"Исключить недоступный прежний K1 и выбрать другой",
|
||
],
|
||
["scan-select-connect", "Выполнить новый поиск и выбрать результат"],
|
||
["manual-recovery-required", "Требуется ручное восстановление"],
|
||
]);
|
||
for (const [safeNextAction, expectedLabel] of labels) {
|
||
const attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: `attempt-${safeNextAction}`,
|
||
connection_mode: "bridge",
|
||
status: "failed",
|
||
stage: "device-info-failed",
|
||
public_error_code: "connection-not-ready",
|
||
side_effect_status: "none",
|
||
safe_next_action: safeNextAction,
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: OBSERVED_AT,
|
||
timeline: [],
|
||
};
|
||
const markup = renderToStaticMarkup(createElement(K1OperatorError, {
|
||
message: "Связь не подтверждена",
|
||
attempt,
|
||
onRefresh() {},
|
||
onClear() {},
|
||
}));
|
||
assert.match(markup, new RegExp(expectedLabel));
|
||
}
|
||
});
|
||
|
||
test("operator banner never renders an unstructured legacy connection error", () => {
|
||
const markup = renderToStaticMarkup(createElement(K1OperatorError, {
|
||
message:
|
||
"K1: исходное устройство не найдено; подключаемся к сохранённому устройству",
|
||
onRefresh() {},
|
||
onClear() {},
|
||
}));
|
||
|
||
assert.match(
|
||
markup,
|
||
/Подключение не завершено\. Автоматического повтора не было/,
|
||
);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
test("compact connection error can expose only its contextual recovery path", () => {
|
||
const markup = renderToStaticMarkup(createElement(K1OperatorError, {
|
||
message: "hidden transport error",
|
||
compact: true,
|
||
showDefaultActions: false,
|
||
recoveryActions: createElement("button", null, "Подключить новый K1"),
|
||
onRefresh() {},
|
||
onClear() {},
|
||
}));
|
||
|
||
assert.match(markup, /Подключение не завершено/);
|
||
assert.match(markup, />Подключить новый K1<\/button>/);
|
||
assert.doesNotMatch(markup, /Проверить состояние|>Закрыть<\/button>/);
|
||
});
|
||
|
||
test("connection attempt network phase copy distinguishes absent, applied and unknown outcomes", () => {
|
||
assert.equal(
|
||
attemptNetworkPhaseLabel("network_not_applied"),
|
||
"Настройки сети не применены",
|
||
);
|
||
assert.equal(
|
||
attemptNetworkPhaseLabel("network_applied"),
|
||
"Настройки сети применены",
|
||
);
|
||
assert.equal(
|
||
attemptNetworkPhaseLabel("network_outcome_unknown"),
|
||
"Результат применения настроек сети не подтверждён",
|
||
);
|
||
});
|
||
|
||
test("runtime errors borrow diagnostics only from their exact Connect attempt", () => {
|
||
const state = durableTopologyState();
|
||
state.snapshot_runtime_id = "runtime-correlated-error";
|
||
state.connection_attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: "op-exact-connect",
|
||
connection_mode: "bridge",
|
||
status: "failed",
|
||
phase: "network_outcome_unknown",
|
||
control_state: "unknown",
|
||
stage: "ble-write-dispatched",
|
||
public_error_code: null,
|
||
side_effect_status: "unknown",
|
||
safe_next_action: "verify-control-read-only",
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: OBSERVED_AT,
|
||
timeline: [],
|
||
};
|
||
const exact = {
|
||
action: "connect",
|
||
runtimeId: state.snapshot_runtime_id,
|
||
leaseGeneration: 0,
|
||
connectionAttemptId: state.connection_attempt.attempt_id,
|
||
};
|
||
|
||
assert.equal(connectionAttemptForRuntimeError(exact, state), state.connection_attempt);
|
||
for (const mismatch of [
|
||
{ ...exact, action: "scan" },
|
||
{ ...exact, runtimeId: "runtime-replaced" },
|
||
{ ...exact, connectionAttemptId: "op-another-connect" },
|
||
{ ...exact, connectionAttemptId: null },
|
||
]) {
|
||
assert.equal(connectionAttemptForRuntimeError(mismatch, state), null);
|
||
}
|
||
assert.equal(
|
||
connectionAttemptForRuntimeError(exact, {
|
||
...state,
|
||
connection_attempt: { ...state.connection_attempt, status: "running" },
|
||
}),
|
||
null,
|
||
);
|
||
});
|
||
|
||
test("connection recovery keeps current then durable priority over a stale fresh recommendation", () => {
|
||
const state = durableTopologyState();
|
||
state.snapshot_runtime_id = "runtime-recovery-priority";
|
||
state.devices = [{
|
||
device_id: "fresh-k1",
|
||
name: "Fresh K1",
|
||
connectable: true,
|
||
likely_k1: true,
|
||
}];
|
||
const decision = (target_source, required_transport_ref) => ({
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source,
|
||
required_transport_ref,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: target_source === "fresh-scan",
|
||
automatic_retry: false,
|
||
});
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
recommended_action: "observe-fresh-device-network",
|
||
allowed_actions: [
|
||
"observe-current-device-network",
|
||
"observe-configured-device-network",
|
||
"observe-fresh-device-network",
|
||
],
|
||
actions: {
|
||
"observe-current-device-network": decision(
|
||
"retained-current-process",
|
||
"current-k1",
|
||
),
|
||
"observe-configured-device-network": decision(
|
||
"durable-configured-state",
|
||
"configured-k1",
|
||
),
|
||
"observe-fresh-device-network": decision("fresh-scan", "fresh-k1"),
|
||
},
|
||
};
|
||
|
||
const recommended = recommendedConnectionRecoveryObservationTarget(state);
|
||
assert.equal(recommended?.action, "observe-current-device-network");
|
||
assert.equal(recommended?.deviceId, "current-k1");
|
||
|
||
state.connection_policy.actions["observe-current-device-network"].allowed = false;
|
||
const durable = recommendedConnectionRecoveryObservationTarget(state);
|
||
assert.equal(durable?.action, "observe-configured-device-network");
|
||
assert.equal(durable?.deviceId, "configured-k1");
|
||
|
||
state.connection_policy.actions["observe-configured-device-network"].allowed = false;
|
||
const fresh = recommendedConnectionRecoveryObservationTarget(state);
|
||
assert.equal(fresh?.action, "observe-fresh-device-network");
|
||
assert.equal(fresh?.deviceId, "fresh-k1");
|
||
|
||
state.connection_policy.recommended_action = "scan-ble";
|
||
state.connection_policy.actions["observe-current-device-network"].allowed = true;
|
||
const fallback = recommendedConnectionRecoveryObservationTarget(state);
|
||
assert.equal(fallback?.action, "observe-current-device-network");
|
||
assert.equal(fallback?.deviceId, "current-k1");
|
||
assert.equal(connectionRecoveryObservationTargetMatches(fallback, fallback), true);
|
||
assert.equal(
|
||
connectionRecoveryObservationTargetMatches(fallback, {
|
||
...fallback,
|
||
deviceId: "foreign-k1",
|
||
}),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("cold Quick recovery dispatches exact Verify without borrowing the mode draft", async () => {
|
||
const target = {
|
||
action: "observe-configured-device-network",
|
||
deviceId: "configured-quick-k1",
|
||
connectionMode: "quick-connect",
|
||
source: "durable-configured-state",
|
||
serverBound: true,
|
||
expectedDiscoveryGeneration: null,
|
||
};
|
||
let calls = 0;
|
||
const dispatched = await dispatchConnectionRecoveryObservationForCurrentRuntime(
|
||
"runtime-cold-quick",
|
||
target,
|
||
(runtimeId) => runtimeId === "runtime-cold-quick",
|
||
() => ({ ...target }),
|
||
async (exactTarget) => {
|
||
calls += 1;
|
||
assert.equal(exactTarget.connectionMode, "quick-connect");
|
||
return "verified";
|
||
},
|
||
);
|
||
assert.deepEqual(dispatched, { dispatched: true, result: "verified" });
|
||
assert.equal(calls, 1);
|
||
|
||
for (const [runtimeCurrent, currentTarget] of [
|
||
[false, target],
|
||
[true, { ...target, deviceId: "replacement-k1" }],
|
||
]) {
|
||
const blocked = await dispatchConnectionRecoveryObservationForCurrentRuntime(
|
||
"runtime-cold-quick",
|
||
target,
|
||
() => runtimeCurrent,
|
||
() => currentTarget,
|
||
async () => {
|
||
calls += 1;
|
||
return "must-not-dispatch";
|
||
},
|
||
);
|
||
assert.deepEqual(blocked, { dispatched: false, result: null });
|
||
}
|
||
assert.equal(calls, 1);
|
||
});
|
||
|
||
test("successful recovery Refresh clears only the compact connection failure", async () => {
|
||
let clearCalls = 0;
|
||
const refreshed = await clearConnectionFailureAfterSuccessfulRefresh(
|
||
async () => ({ snapshot_runtime_id: "runtime-refreshed" }),
|
||
() => {
|
||
clearCalls += 1;
|
||
},
|
||
);
|
||
assert.equal(refreshed, true);
|
||
assert.equal(clearCalls, 1);
|
||
|
||
const unavailable = await clearConnectionFailureAfterSuccessfulRefresh(
|
||
async () => null,
|
||
() => {
|
||
clearCalls += 1;
|
||
},
|
||
);
|
||
assert.equal(unavailable, false);
|
||
assert.equal(clearCalls, 1);
|
||
});
|
||
|
||
test("successful recovery Scan survives configured-to-fresh promotion for the same K1", () => {
|
||
const attempt = terminalConnectionRecoveryState().connection_attempt;
|
||
const configuredTarget = {
|
||
action: "observe-configured-device-network",
|
||
deviceId: "F89438FA-55ED-85AD-EED7-734AC84746D8",
|
||
connectionMode: "bridge",
|
||
source: "durable-configured-state",
|
||
serverBound: true,
|
||
expectedDiscoveryGeneration: null,
|
||
};
|
||
const freshTarget = {
|
||
...configuredTarget,
|
||
action: "observe-fresh-device-network",
|
||
deviceId: "f89438fa-55ed-85ad-eed7-734ac84746d8",
|
||
source: "fresh-scan",
|
||
expectedDiscoveryGeneration: 28,
|
||
};
|
||
const beforeScan = connectionRecoveryEscapeKey({
|
||
snapshotRuntimeId: "runtime-recovery-scan",
|
||
attempt,
|
||
target: configuredTarget,
|
||
});
|
||
const afterSuccessfulScan = connectionRecoveryEscapeKey({
|
||
snapshotRuntimeId: "runtime-recovery-scan",
|
||
attempt,
|
||
target: freshTarget,
|
||
});
|
||
|
||
assert.equal(afterSuccessfulScan, beforeScan);
|
||
const successfulEscape = connectionRecoveryEscapeAfterScan(true, beforeScan);
|
||
// Success stores the pre-Scan key, so the promoted fresh results own Step 01.
|
||
assert.equal(
|
||
connectionRecoveryIsRequired(afterSuccessfulScan, successfulEscape),
|
||
false,
|
||
);
|
||
// A failed Scan stores no escape and leaves the recovery card authoritative.
|
||
const failedEscape = connectionRecoveryEscapeAfterScan(false, beforeScan);
|
||
assert.equal(failedEscape, null);
|
||
assert.equal(connectionRecoveryIsRequired(afterSuccessfulScan, failedEscape), true);
|
||
|
||
for (const changed of [
|
||
{
|
||
snapshotRuntimeId: "runtime-replaced",
|
||
attempt,
|
||
target: freshTarget,
|
||
},
|
||
{
|
||
snapshotRuntimeId: "runtime-recovery-scan",
|
||
attempt: { ...attempt, attempt_id: "op-new-attempt" },
|
||
target: freshTarget,
|
||
},
|
||
{
|
||
snapshotRuntimeId: "runtime-recovery-scan",
|
||
attempt,
|
||
target: { ...freshTarget, deviceId: "replacement-k1" },
|
||
},
|
||
{
|
||
snapshotRuntimeId: "runtime-recovery-scan",
|
||
attempt,
|
||
target: { ...freshTarget, connectionMode: "quick-connect" },
|
||
},
|
||
]) {
|
||
assert.equal(
|
||
connectionRecoveryIsRequired(
|
||
connectionRecoveryEscapeKey(changed),
|
||
beforeScan,
|
||
),
|
||
true,
|
||
);
|
||
}
|
||
});
|
||
|
||
test("metric cards render values only for replay or supervisor-authoritative live data", () => {
|
||
const legacy = runtimeState();
|
||
delete legacy.connection_supervisor;
|
||
const legacyMarkup = renderToStaticMarkup(createElement(K1Metrics, {
|
||
controller: { state: legacy },
|
||
}));
|
||
assert.doesNotMatch(legacyMarkup, /12,5/);
|
||
assert.match(legacyMarkup, /ДО ПУБЛИКАЦИИ/);
|
||
|
||
const authoritativeMarkup = renderToStaticMarkup(createElement(K1Metrics, {
|
||
controller: { state: runtimeState() },
|
||
}));
|
||
assert.match(authoritativeMarkup, /12,5/);
|
||
assert.match(authoritativeMarkup, /42[\s ]000/);
|
||
assert.match(authoritativeMarkup, /Данные потока при этом сохраняются/);
|
||
assert.doesNotMatch(authoritativeMarkup, /Исходные данные/);
|
||
assertCanonicalConnectionCopy(authoritativeMarkup);
|
||
});
|
||
|
||
test("cold disconnected connection SSR hides every operational panel", () => {
|
||
const state = durableTopologyState();
|
||
const markup = renderConnectionPipelines(state);
|
||
const unresolvedHistory = reopenedPhysicalState();
|
||
const unresolvedHistoryMarkup = renderConnectionPipelines(unresolvedHistory);
|
||
|
||
assert.equal(shouldRenderK1OperationalPanels(state), false);
|
||
assert.equal(
|
||
shouldRenderK1OperationalPanels(unresolvedHistory),
|
||
false,
|
||
"unresolved durable physical history is handled inside the connection pipeline",
|
||
);
|
||
assert.match(markup, /class="[^"]*\bconnection-panel\b/);
|
||
assert.match(markup, /ПОДКЛЮЧЕНИЕ · ШАГИ 01–02/);
|
||
assert.doesNotMatch(markup, /class="metrics-grid/);
|
||
assert.doesNotMatch(markup, /class="[^"]*\bsession-panel\b/);
|
||
assert.doesNotMatch(markup, /class="diagnostics-grid/);
|
||
assert.doesNotMatch(markup, /class="device-workspace__side/);
|
||
assert.doesNotMatch(unresolvedHistoryMarkup, /class="metrics-grid/);
|
||
assert.doesNotMatch(unresolvedHistoryMarkup, /class="[^"]*\bsession-panel\b/);
|
||
assert.doesNotMatch(unresolvedHistoryMarkup, /class="diagnostics-grid/);
|
||
assert.doesNotMatch(unresolvedHistoryMarkup, /class="device-workspace__side/);
|
||
});
|
||
|
||
test("live preparation keeps operational panels mounted across a control handoff", () => {
|
||
const state = durableTopologyState();
|
||
const markup = renderConnectionPipelines(state, "live");
|
||
|
||
assert.equal(shouldRenderK1OperationalPanels(state, "live"), true);
|
||
assert.match(markup, /class="metrics-grid/);
|
||
assert.match(markup, /class="[^"]*\bsession-panel\b/);
|
||
assert.match(markup, /class="diagnostics-grid/);
|
||
assert.match(markup, /class="device-workspace__side/);
|
||
assert.match(markup, /Подключение установлено/);
|
||
assert.doesNotMatch(markup, /Переподключиться|Подключить заново/);
|
||
});
|
||
|
||
test("connected, active, replay and recovery SSR retain operational panels", () => {
|
||
const connected = runtimeState();
|
||
connected.phase = "connected";
|
||
connected.source_mode = "idle";
|
||
connected.acquisition = null;
|
||
|
||
const active = runtimeState();
|
||
delete active.connection_supervisor;
|
||
|
||
const replay = runtimeState();
|
||
replay.phase = "replay";
|
||
replay.source_mode = "replay";
|
||
replay.acquisition = null;
|
||
delete replay.connection_supervisor;
|
||
|
||
const recovery = durableTopologyState();
|
||
recovery.acquisition = {
|
||
acquisition_id: "acquisition-recovery",
|
||
device_id: "device-recovery",
|
||
device_session_id: "session-recovery",
|
||
compatibility_profile_id: PROFILE_ID,
|
||
control_mode: "plugin-commanded",
|
||
requested_streams: ["spatial.point-cloud.live"],
|
||
target_host: "127.0.0.1",
|
||
duration_seconds: 0,
|
||
evidence_policy: "required",
|
||
state: "failed",
|
||
state_revision: 8,
|
||
cleanup_pending: true,
|
||
};
|
||
|
||
for (const [name, state] of [
|
||
["connected", connected],
|
||
["active", active],
|
||
["replay", replay],
|
||
["recovery", recovery],
|
||
]) {
|
||
assert.equal(
|
||
shouldRenderK1OperationalPanels(state),
|
||
true,
|
||
`${name} must keep operational controls visible`,
|
||
);
|
||
const markup = renderConnectionPipelines(state);
|
||
assert.match(markup, /class="metrics-grid/, name);
|
||
assert.match(markup, /class="[^"]*\bsession-panel\b/, name);
|
||
assert.match(markup, /class="diagnostics-grid/, name);
|
||
assert.match(markup, /class="device-workspace__side/, name);
|
||
}
|
||
});
|
||
|
||
test("local connection shell scopes process-only configuring and connected labels", () => {
|
||
assert.equal(shellPresentation.localConnectionPhaseLabel("configuring"), "Подключение");
|
||
assert.equal(
|
||
shellPresentation.localConnectionPhaseLabel("connected"),
|
||
"Подключение установлено",
|
||
);
|
||
assert.equal(
|
||
shellPresentation.phaseLabel("configuring"),
|
||
"Настройка устройства",
|
||
"other workspaces keep the global runtime label",
|
||
);
|
||
assert.equal(
|
||
shellPresentation.phaseLabel("connected"),
|
||
"Устройство подключено",
|
||
"other workspaces keep the global runtime label",
|
||
);
|
||
});
|
||
|
||
test("replay and local-only stop branches use bounded process copy", () => {
|
||
const replayState = runtimeState();
|
||
replayState.phase = "replay";
|
||
replayState.source_mode = "replay";
|
||
const replayMarkup = renderToStaticMarkup(createElement(K1AcquisitionPipeline, {
|
||
controller: acquisitionController(replayState),
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
}));
|
||
assert.match(replayMarkup, /Локальный файл записи/);
|
||
assert.doesNotMatch(replayMarkup, /Локальный файл исходных данных/);
|
||
assertCanonicalConnectionCopy(replayMarkup);
|
||
|
||
const stopState = runtimeState();
|
||
stopState.compatibility.vendor_writes_enabled = false;
|
||
stopState.acquisition.control_mode = "operator-manual";
|
||
stopState.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["stop-local-receiver"],
|
||
actions: {
|
||
"stop-local-receiver": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "local-runtime",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
const stopMarkup = renderToStaticMarkup(createElement(K1AcquisitionPipeline, {
|
||
controller: acquisitionController(stopState),
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
}));
|
||
assert.match(stopMarkup, /Состояние сканирования остаётся неизвестным/);
|
||
assert.doesNotMatch(stopMarkup, /Физическое состояние сканера/);
|
||
assertCanonicalConnectionCopy(stopMarkup);
|
||
});
|
||
|
||
test("software-commanded calibration exposes no teardown until canonical STOP is safe", () => {
|
||
const state = runtimeState();
|
||
state.snapshot_runtime_id = "runtime-calibration-001";
|
||
state.snapshot_revision = 31;
|
||
state.phase = "starting_live";
|
||
state.source_mode = "live";
|
||
state.acquisition.state = "awaiting_external_start";
|
||
state.acquisition.state_revision = 3;
|
||
state.application_control_session = {
|
||
session_generation: 5,
|
||
state_revision: 7,
|
||
state: "device-initializing",
|
||
can_stop: false,
|
||
control_socket_open: true,
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["stop-local-receiver"],
|
||
actions: {
|
||
"stop-acquisition": {
|
||
allowed: false,
|
||
reason_codes: ["supervisor-action-not-allowed"],
|
||
target_source: "connection-supervisor",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
"stop-local-receiver": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "local-runtime",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
const pipelineMarkup = renderToStaticMarkup(createElement(K1AcquisitionPipeline, {
|
||
controller: acquisitionController(state),
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
}));
|
||
const spatialMarkup = renderToStaticMarkup(createElement(K1SpatialControlsView, {
|
||
controller: acquisitionController(state),
|
||
}));
|
||
|
||
assert.match(pipelineMarkup, /Кнопка остановки появится только после подтверждённого SCANNING/);
|
||
assert.match(spatialMarkup, /K1 калибруется и готовит облако точек/);
|
||
for (const markup of [pipelineMarkup, spatialMarkup]) {
|
||
assert.equal(buttonMarkupWithText(markup, "Завершить локальный приём").length, 0);
|
||
assert.equal(buttonMarkupWithText(markup, "Аварийно завершить локальный приём").length, 0);
|
||
assert.equal(buttonMarkupWithText(markup, "Остановить устройство и запись").length, 0);
|
||
assert.equal(buttonMarkupWithText(markup, "Отменить запуск до START").length, 0);
|
||
}
|
||
});
|
||
|
||
test("physical STOP is fail-closed across both acquisition surfaces", () => {
|
||
const state = runtimeState();
|
||
state.snapshot_runtime_id = "runtime-stop-001";
|
||
state.snapshot_revision = 40;
|
||
state.application_control_session = {
|
||
session_generation: 5,
|
||
state_revision: 8,
|
||
state: "scanning",
|
||
can_stop: true,
|
||
control_socket_open: true,
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["stop-acquisition", "stop-local-receiver"],
|
||
actions: {
|
||
"stop-acquisition": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "connection-supervisor",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
"stop-local-receiver": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "local-runtime",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
assert.equal(canIssueCanonicalStop(state, false), true);
|
||
assert.equal(canIssueCanonicalStop(state, true), false);
|
||
const missingRuntimeIdentity = structuredClone(state);
|
||
delete missingRuntimeIdentity.snapshot_runtime_id;
|
||
assert.equal(physicalStopIntentCheckpoint(missingRuntimeIdentity), null);
|
||
assert.equal(canIssueCanonicalStop(missingRuntimeIdentity, false), false);
|
||
const zeroGeneration = structuredClone(state);
|
||
zeroGeneration.application_control_session.session_generation = 0;
|
||
assert.equal(physicalStopIntentCheckpoint(zeroGeneration), null);
|
||
const zeroRevision = structuredClone(state);
|
||
zeroRevision.application_control_session.state_revision = 0;
|
||
assert.equal(physicalStopIntentCheckpoint(zeroRevision), null);
|
||
const spentCheckpoint = physicalStopIntentCheckpoint(state);
|
||
assert.ok(spentCheckpoint);
|
||
const sameCasPoll = structuredClone(state);
|
||
sameCasPoll.snapshot_revision += 1;
|
||
assert.equal(
|
||
authoritativeStateSupersedesPhysicalStopIntent(spentCheckpoint, sameCasPoll),
|
||
false,
|
||
"a newer polling snapshot with the same control CAS must stay spent",
|
||
);
|
||
const nextControlTransition = structuredClone(sameCasPoll);
|
||
nextControlTransition.application_control_session.state_revision += 1;
|
||
assert.equal(
|
||
authoritativeStateSupersedesPhysicalStopIntent(
|
||
spentCheckpoint,
|
||
nextControlTransition,
|
||
),
|
||
true,
|
||
"a newer atomic snapshot plus a newer exact control CAS is fresh authority",
|
||
);
|
||
const sameTargetReplacement = structuredClone(state);
|
||
sameTargetReplacement.snapshot_runtime_id = "runtime-stop-002";
|
||
sameTargetReplacement.snapshot_revision = 1;
|
||
assert.equal(
|
||
authoritativeStateSupersedesPhysicalStopIntent(
|
||
spentCheckpoint,
|
||
sameTargetReplacement,
|
||
),
|
||
true,
|
||
"a newer accepted runtime with an exact STOP gate is fresh authority",
|
||
);
|
||
const nonAuthoritativeReplacement = structuredClone(sameTargetReplacement);
|
||
nonAuthoritativeReplacement.application_control_session.can_stop = false;
|
||
assert.equal(
|
||
authoritativeStateSupersedesPhysicalStopIntent(
|
||
spentCheckpoint,
|
||
nonAuthoritativeReplacement,
|
||
),
|
||
false,
|
||
"runtime replacement without an exact current STOP gate stays spent",
|
||
);
|
||
const distinctAcquisition = structuredClone(sameCasPoll);
|
||
distinctAcquisition.acquisition.acquisition_id = "acquisition-002";
|
||
assert.equal(
|
||
authoritativeStateSupersedesPhysicalStopIntent(
|
||
spentCheckpoint,
|
||
distinctAcquisition,
|
||
),
|
||
true,
|
||
"a newer accepted snapshot may admit a distinct acquisition target",
|
||
);
|
||
|
||
state.application_control_session.state = "failed";
|
||
assert.equal(canIssueCanonicalStop(state, false), false);
|
||
state.application_control_session.state = "scanning";
|
||
state.application_control_session.can_stop = false;
|
||
assert.equal(canIssueCanonicalStop(state, false), false);
|
||
state.application_control_session.can_stop = true;
|
||
state.connection_policy.actions["stop-acquisition"].allowed = false;
|
||
state.connection_policy.actions["stop-acquisition"].reason_codes = [
|
||
"physical-control-authority-unavailable",
|
||
];
|
||
state.connection_policy.allowed_actions = ["stop-local-receiver"];
|
||
assert.equal(canIssueCanonicalStop(state, false), false);
|
||
|
||
state.connection_policy.actions["stop-acquisition"].allowed = true;
|
||
state.connection_policy.actions["stop-acquisition"].reason_codes = [];
|
||
state.connection_policy.allowed_actions = [
|
||
"stop-acquisition",
|
||
"stop-local-receiver",
|
||
];
|
||
const physicalMarkup = renderToStaticMarkup(createElement(K1AcquisitionPipeline, {
|
||
controller: acquisitionController(state),
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
}));
|
||
const physicalButtons = buttonMarkupWithText(
|
||
physicalMarkup,
|
||
"Остановить устройство и запись",
|
||
);
|
||
assert.equal(physicalButtons.length, 1);
|
||
assert.doesNotMatch(physicalButtons[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.doesNotMatch(physicalMarkup, /Завершить локальный приём/);
|
||
|
||
const unrelatedErrorMarkup = renderToStaticMarkup(createElement(
|
||
K1AcquisitionPipeline,
|
||
{
|
||
controller: {
|
||
...acquisitionController(state),
|
||
error: "unrelated local viewer error",
|
||
},
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
},
|
||
));
|
||
assert.equal(
|
||
buttonMarkupWithText(
|
||
unrelatedErrorMarkup,
|
||
"Остановить устройство и запись",
|
||
).length,
|
||
1,
|
||
"a presentation error that did not spend physical STOP must not hide it",
|
||
);
|
||
|
||
const dismissedFailureMarkup = renderToStaticMarkup(createElement(
|
||
K1AcquisitionPipeline,
|
||
{
|
||
controller: {
|
||
...acquisitionController(state),
|
||
// Closing a presentation banner clears `error`; the independent spent
|
||
// checkpoint must continue to suppress the same physical mutation.
|
||
error: null,
|
||
physicalStopIntentSpent: true,
|
||
},
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
},
|
||
));
|
||
assert.doesNotMatch(dismissedFailureMarkup, /Остановить устройство и запись/);
|
||
const localButtons = buttonMarkupWithText(
|
||
dismissedFailureMarkup,
|
||
"Завершить локальный приём",
|
||
);
|
||
assert.equal(localButtons.length, 0);
|
||
assert.match(
|
||
dismissedFailureMarkup,
|
||
/Повторная команда и локальное завершение заблокированы/,
|
||
);
|
||
assertCanonicalConnectionCopy(dismissedFailureMarkup);
|
||
|
||
const classifiedStopState = structuredClone(state);
|
||
classifiedStopState.acquisition.state = "awaiting_external_stop";
|
||
classifiedStopState.connection_policy.actions["stop-acquisition"].allowed = false;
|
||
classifiedStopState.connection_policy.allowed_actions = ["stop-local-receiver"];
|
||
const classifiedStopMarkup = renderToStaticMarkup(createElement(
|
||
K1SpatialControlsView,
|
||
{
|
||
controller: {
|
||
...acquisitionController(classifiedStopState),
|
||
error: "STOP не был отправлен; требуется read-only восстановление",
|
||
physicalStopIntentSpent: true,
|
||
},
|
||
},
|
||
));
|
||
assert.equal(
|
||
buttonMarkupWithText(classifiedStopMarkup, "Завершить локальный приём").length,
|
||
0,
|
||
"a classified STOP must not leave a permanently disabled local action in the scene",
|
||
);
|
||
|
||
const localCleanupPendingMarkup = renderToStaticMarkup(createElement(
|
||
K1AcquisitionPipeline,
|
||
{
|
||
controller: {
|
||
...acquisitionController(state),
|
||
pendingAction: "stop",
|
||
error: null,
|
||
physicalStopIntentSpent: true,
|
||
},
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
},
|
||
));
|
||
assert.doesNotMatch(localCleanupPendingMarkup, /Завершение локального приёма…/);
|
||
assert.match(
|
||
localCleanupPendingMarkup,
|
||
/Повторная команда и локальное завершение заблокированы/,
|
||
);
|
||
assert.doesNotMatch(localCleanupPendingMarkup, /Остановка устройства…/);
|
||
|
||
const physicalStopPendingMarkup = renderToStaticMarkup(createElement(
|
||
K1AcquisitionPipeline,
|
||
{
|
||
controller: {
|
||
...acquisitionController(state),
|
||
pendingAction: "stop",
|
||
physicalStopIntentSpent: true,
|
||
physicalStopInFlight: true,
|
||
},
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
},
|
||
));
|
||
assert.match(physicalStopPendingMarkup, /Остановка устройства…/);
|
||
assert.doesNotMatch(physicalStopPendingMarkup, /Завершение локального приёма…/);
|
||
|
||
const terminalDeniedState = structuredClone(state);
|
||
terminalDeniedState.phase = "error";
|
||
terminalDeniedState.source_mode = "idle";
|
||
terminalDeniedState.acquisition.state = "failed";
|
||
terminalDeniedState.acquisition.cleanup_pending = true;
|
||
terminalDeniedState.connection_policy.actions["stop-acquisition"].allowed = false;
|
||
terminalDeniedState.connection_policy.actions["stop-acquisition"].reason_codes = [
|
||
"physical-control-authority-unavailable",
|
||
];
|
||
terminalDeniedState.connection_policy.allowed_actions = ["stop-local-receiver"];
|
||
const terminalDeniedMarkup = renderToStaticMarkup(createElement(
|
||
K1AcquisitionPipeline,
|
||
{
|
||
controller: acquisitionController(terminalDeniedState),
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
},
|
||
));
|
||
assert.match(terminalDeniedMarkup, /ВОССТАНОВЛЕНИЕ · ЛОКАЛЬНЫЙ КОНТУР/);
|
||
assert.match(terminalDeniedMarkup, /Доступно локальное завершение приёма/);
|
||
assert.doesNotMatch(terminalDeniedMarkup, /Локальная запись завершена/);
|
||
assert.doesNotMatch(
|
||
terminalDeniedMarkup,
|
||
/явный STOP|Остановить сканирование|Сканирование продолжается|Требуется остановка/,
|
||
);
|
||
|
||
const terminalSpentState = structuredClone(terminalDeniedState);
|
||
terminalSpentState.connection_policy.actions["stop-acquisition"].allowed = true;
|
||
terminalSpentState.connection_policy.actions["stop-acquisition"].reason_codes = [];
|
||
terminalSpentState.connection_policy.allowed_actions = [
|
||
"stop-acquisition",
|
||
"stop-local-receiver",
|
||
];
|
||
const terminalReadyMarkup = renderToStaticMarkup(createElement(
|
||
K1AcquisitionPipeline,
|
||
{
|
||
controller: acquisitionController(terminalSpentState),
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
},
|
||
));
|
||
assert.match(terminalReadyMarkup, /Локальный приём ещё требует завершения/);
|
||
assert.doesNotMatch(terminalReadyMarkup, /Локальная запись (?:уже )?завершена/);
|
||
const terminalSpentMarkup = renderToStaticMarkup(createElement(
|
||
K1AcquisitionPipeline,
|
||
{
|
||
controller: {
|
||
...acquisitionController(terminalSpentState),
|
||
error: null,
|
||
physicalStopIntentSpent: true,
|
||
},
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
},
|
||
));
|
||
assert.match(terminalSpentMarkup, /ВОССТАНОВЛЕНИЕ · ЛОКАЛЬНЫЙ КОНТУР/);
|
||
assert.match(terminalSpentMarkup, /Повторная команда K1 не отправляется/);
|
||
assert.doesNotMatch(
|
||
terminalSpentMarkup,
|
||
/явный STOP|Остановить сканирование|Сканирование продолжается|Требуется остановка/,
|
||
);
|
||
|
||
const terminalInFlightMarkup = renderToStaticMarkup(createElement(
|
||
K1AcquisitionPipeline,
|
||
{
|
||
controller: {
|
||
...acquisitionController(terminalSpentState),
|
||
pendingAction: "stop",
|
||
physicalStopIntentSpent: true,
|
||
physicalStopInFlight: true,
|
||
},
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
},
|
||
));
|
||
assert.match(terminalInFlightMarkup, /ВОССТАНОВЛЕНИЕ · КОМАНДА ОТПРАВЛЕНА/);
|
||
assert.match(terminalInFlightMarkup, /Команда остановки устройства уже отправлена/);
|
||
assert.match(terminalInFlightMarkup, /Остановка устройства…/);
|
||
assert.doesNotMatch(
|
||
terminalInFlightMarkup,
|
||
/требуется явный STOP|Нажмите «Остановить сканирование»/,
|
||
);
|
||
|
||
const noStopActionState = structuredClone(state);
|
||
noStopActionState.connection_policy.actions["stop-acquisition"].allowed = false;
|
||
noStopActionState.connection_policy.actions["stop-acquisition"].reason_codes = [
|
||
"physical-control-authority-unavailable",
|
||
];
|
||
noStopActionState.connection_policy.actions["stop-local-receiver"].allowed = false;
|
||
noStopActionState.connection_policy.actions["stop-local-receiver"].reason_codes = [
|
||
"local-acquisition-receiver-not-active",
|
||
];
|
||
noStopActionState.connection_policy.allowed_actions = [];
|
||
noStopActionState.connection_policy.recommended_action = "wait-for-operation";
|
||
noStopActionState.phase = "error";
|
||
noStopActionState.source_mode = "idle";
|
||
noStopActionState.acquisition.state = "failed";
|
||
noStopActionState.acquisition.cleanup_pending = true;
|
||
const noStopActionMarkup = renderToStaticMarkup(createElement(
|
||
K1AcquisitionPipeline,
|
||
{
|
||
controller: acquisitionController(noStopActionState),
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
},
|
||
));
|
||
assert.equal(
|
||
buttonMarkupWithText(noStopActionMarkup, "Завершить локальный приём").length,
|
||
0,
|
||
);
|
||
assert.equal(
|
||
buttonMarkupWithText(noStopActionMarkup, "Остановить устройство и запись").length,
|
||
0,
|
||
);
|
||
assert.match(
|
||
noStopActionMarkup,
|
||
/ВОССТАНОВЛЕНИЕ · ТОЛЬКО ЧТЕНИЕ/,
|
||
);
|
||
assert.match(noStopActionMarkup, /Доступно только read-only восстановление/);
|
||
assert.doesNotMatch(
|
||
noStopActionMarkup,
|
||
/Доступно локальное завершение|Завершите локальный приём|явный STOP|Остановить сканирование/,
|
||
);
|
||
});
|
||
|
||
test("recovered physical SCANNING overrides replay with one explicit K1 STOP", () => {
|
||
const state = runtimeState();
|
||
state.snapshot_runtime_id = "runtime-recovered-stop";
|
||
state.snapshot_revision = 12;
|
||
state.phase = "replay";
|
||
state.source_mode = "replay";
|
||
state.acquisition = {
|
||
...state.acquisition,
|
||
state: "failed",
|
||
state_revision: 9,
|
||
cleanup_pending: false,
|
||
};
|
||
state.application_control_session = {
|
||
session_generation: 4,
|
||
state_revision: 11,
|
||
state: "scanning",
|
||
can_stop: true,
|
||
control_socket_open: true,
|
||
physical_command: {
|
||
requires_reconciliation: false,
|
||
resolved_active_recovery_required: true,
|
||
observed_session_state: "scanning",
|
||
},
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["stop-acquisition"],
|
||
actions: {
|
||
"stop-acquisition": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "connection-supervisor",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
const markup = renderToStaticMarkup(createElement(K1AcquisitionPipeline, {
|
||
controller: acquisitionController(state),
|
||
desiredConnectionMode: "bridge",
|
||
openSpatialScene() {},
|
||
activateAutomaticSpatialSource() {},
|
||
}));
|
||
|
||
assert.match(markup, /ВОССТАНОВЛЕНИЕ · ОСТАНОВКА/);
|
||
assert.match(markup, /Сканирование продолжается/);
|
||
assert.match(markup, /Требуется остановка/);
|
||
assert.match(markup, /Сканирование подтверждено; требуется явный STOP/);
|
||
assert.doesNotMatch(markup, /Назовите проект и запустите приём/);
|
||
assert.doesNotMatch(markup, /Путь к записи|Запустить повтор записи/);
|
||
assert.doesNotMatch(markup, /Подготовить проект|Запустить сканирование/);
|
||
|
||
const stopButtons = buttonMarkupWithText(markup, "Остановить сканирование");
|
||
assert.equal(stopButtons.length, 1);
|
||
assert.doesNotMatch(stopButtons[0], /\bdisabled(?:=|\s|>)/);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
|
||
|
||
|
||
|
||
|
||
test("authoritative Bridge renders all completed steps with canonical process copy", () => {
|
||
const state = runtimeState();
|
||
state.phase = "connected";
|
||
state.source_mode = "idle";
|
||
state.acquisition = null;
|
||
state.devices = [];
|
||
state.message = "Готово. Включите устройство и начните с поиска по Bluetooth.";
|
||
state.connection_supervisor = supervisor({ control: true, data: false });
|
||
state.connection_lifecycle = connectionLifecycle({ control: true });
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["start-acquisition"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: false,
|
||
reason_codes: ["acquisition-active"],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
recommended_action: "start-acquisition",
|
||
};
|
||
|
||
const markup = renderToStaticMarkup(createElement(K1ProvisioningPipeline, {
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
}));
|
||
|
||
assert.doesNotMatch(markup, /Питание|aria-checked=/);
|
||
assert.match(markup, /<span>01<\/span>/);
|
||
assert.match(markup, /<span>02<\/span>/);
|
||
assert.match(markup, /<h3>Подключение<\/h3>/);
|
||
assert.match(markup, /<h3>Сеть<\/h3>/);
|
||
assert.match(markup, /Подключение установлено/);
|
||
assert.match(markup, /<strong>ble-k1-001<\/strong>/);
|
||
assert.match(markup, /<strong>Общая сеть · Bridge<\/strong>/);
|
||
assert.match(markup, /<small>192\.168\.68\.52<\/small>/);
|
||
assert.equal(buttonMarkupWithText(markup, "Выбрать другое").length, 0);
|
||
assert.equal(buttonMarkupWithText(markup, "Изменить сеть").length, 0);
|
||
assert.doesNotMatch(markup, /class="nodedc-activity-indicator"/);
|
||
assert.doesNotMatch(
|
||
markup,
|
||
/Найти устройство Bluetooth|Повторить поиск Bluetooth|Устройства пока не найдены/,
|
||
);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
test("connected Bridge action slots survive transient lifecycle contention A to B to A", () => {
|
||
const actionDecision = (reasonCodes) => ({
|
||
allowed: reasonCodes.length === 0,
|
||
reason_codes: reasonCodes,
|
||
target_source: "local-prestart-handoff",
|
||
required_transport_ref: "ble-k1-001",
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: false,
|
||
device_write_performed: false,
|
||
automatic_retry: false,
|
||
});
|
||
const connectedState = (snapshotRevision, reasonCodes = []) => {
|
||
const state = runtimeState();
|
||
state.snapshot_runtime_id = "runtime-connected-actions";
|
||
state.snapshot_runtime_started_at_utc = "2026-08-11T06:00:00Z";
|
||
state.snapshot_revision = snapshotRevision;
|
||
state.phase = "connected";
|
||
state.source_mode = "idle";
|
||
state.snapshot_runtime_id = "runtime-ready-pending";
|
||
state.acquisition = null;
|
||
state.devices = [];
|
||
state.connection_supervisor = supervisor({ control: true, data: false });
|
||
state.connection_lifecycle = connectionLifecycle({ control: true });
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: reasonCodes.length === 0
|
||
? ["prepare-select-device", "prepare-change-network"]
|
||
: [],
|
||
actions: {
|
||
"prepare-select-device": actionDecision(reasonCodes),
|
||
"prepare-change-network": actionDecision(reasonCodes),
|
||
},
|
||
};
|
||
return state;
|
||
};
|
||
const frames = [
|
||
connectedState(40),
|
||
connectedState(41, [
|
||
"connection-reconfiguration-lifecycle-busy",
|
||
"k1-lifecycle-process-lease-network-owned",
|
||
]),
|
||
connectedState(42),
|
||
];
|
||
|
||
for (const reason of [
|
||
"connection-reconfiguration-lifecycle-busy",
|
||
"k1-lifecycle-process-lease-network-owned",
|
||
]) {
|
||
const singlyContended = connectedState(39, [reason]);
|
||
assert.equal(
|
||
connectedReconfigurationActionApplicable(
|
||
singlyContended,
|
||
"prepare-select-device",
|
||
),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
connectedReconfigurationActionApplicable(
|
||
singlyContended,
|
||
"prepare-change-network",
|
||
),
|
||
true,
|
||
);
|
||
}
|
||
|
||
for (const [index, state] of frames.entries()) {
|
||
assert.equal(
|
||
connectedReconfigurationActionApplicable(state, "prepare-select-device"),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
connectedReconfigurationActionApplicable(state, "prepare-change-network"),
|
||
true,
|
||
);
|
||
const markup = renderToStaticMarkup(createElement(K1ProvisioningPipeline, {
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
}));
|
||
assert.match(markup, /Подключение установлено/);
|
||
const selectAction = buttonMarkupWithText(markup, "Выбрать другое");
|
||
const networkAction = buttonMarkupWithText(markup, "Изменить сеть");
|
||
assert.equal(selectAction.length, 1, `frame ${index} keeps select action slot`);
|
||
assert.equal(networkAction.length, 1, `frame ${index} keeps network action slot`);
|
||
if (index === 1) {
|
||
assert.match(selectAction[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.match(networkAction[0], /\bdisabled(?:=|\s|>)/);
|
||
} else {
|
||
assert.doesNotMatch(selectAction[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.doesNotMatch(networkAction[0], /\bdisabled(?:=|\s|>)/);
|
||
}
|
||
}
|
||
|
||
const durableBlocker = connectedState(43, ["acquisition-active"]);
|
||
assert.equal(
|
||
connectedReconfigurationActionApplicable(
|
||
durableBlocker,
|
||
"prepare-select-device",
|
||
),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
connectedReconfigurationActionApplicable(
|
||
durableBlocker,
|
||
"prepare-change-network",
|
||
),
|
||
false,
|
||
);
|
||
const durableMarkup = renderToStaticMarkup(createElement(K1ProvisioningPipeline, {
|
||
controller: provisioningController(durableBlocker),
|
||
desiredMode: "bridge",
|
||
}));
|
||
assert.equal(buttonMarkupWithText(durableMarkup, "Выбрать другое").length, 0);
|
||
assert.equal(buttonMarkupWithText(durableMarkup, "Изменить сеть").length, 0);
|
||
});
|
||
|
||
for (const pendingAction of ["connect", "verify"]) {
|
||
test(`authoritative Ready with pending ${pendingAction} keeps one canonical progress surface`, () => {
|
||
const state = runtimeState();
|
||
state.phase = "connected";
|
||
state.source_mode = "idle";
|
||
state.snapshot_runtime_id = "runtime-ready-pending";
|
||
state.acquisition = null;
|
||
state.devices = [];
|
||
state.connection_supervisor = supervisor({ control: true, data: false });
|
||
state.connection_lifecycle = connectionLifecycle({ control: true });
|
||
|
||
const markup = renderProvisioningWithCurrentPendingAction({
|
||
controller: {
|
||
...provisioningController(state),
|
||
pendingAction,
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
isConnectionActionAuthorityCurrent: () => true,
|
||
},
|
||
desiredMode: "bridge",
|
||
}, { publicSearch: false });
|
||
|
||
assert.equal(
|
||
(markup.match(/class="nodedc-activity-indicator"/g) ?? []).length,
|
||
1,
|
||
);
|
||
assert.match(markup, /<strong>ble-k1-001<\/strong>/);
|
||
if (pendingAction === "connect") {
|
||
assert.match(markup, /<h3>Сеть<\/h3>/);
|
||
assert.match(markup, /Настройка сети…/);
|
||
} else {
|
||
assert.match(markup, /<h3>Сеть<\/h3>/);
|
||
assert.match(markup, /Проверка подключения без изменения сети…/);
|
||
assert.match(markup, /<span>02<\/span>/);
|
||
}
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
test("explicit read-only recovery keeps its network-unavailable classification", () => {
|
||
const base = {
|
||
connection_verification: {
|
||
status: "endpoint-unreachable",
|
||
lease_state: "configured-unverified",
|
||
network_reachability: "unreachable",
|
||
},
|
||
};
|
||
assert.equal(readOnlyObservationShowsNetworkUnavailable(base), true);
|
||
assert.equal(readOnlyObservationShowsNetworkUnavailable({
|
||
connection_verification: {
|
||
...base.connection_verification,
|
||
status: "tcp-reachable-device-info-unverified",
|
||
network_reachability: "reachable",
|
||
},
|
||
}), false);
|
||
assert.equal(readOnlyObservationShowsNetworkUnavailable(null), false);
|
||
assert.equal(readOnlyObservationShowsNetworkUnavailable({
|
||
connection_verification: {
|
||
status: "device-network-applied",
|
||
lease_state: "configured-unverified",
|
||
network_reachability: "unknown",
|
||
reason_code: "endpoint-target-unconfigured",
|
||
},
|
||
}), true);
|
||
for (const reasonCode of [
|
||
"connection-verify-address-unavailable",
|
||
"connection-verify-connection-missing",
|
||
"connection-verify-route-mismatch",
|
||
"connection-verify-mqtt-unreachable",
|
||
"configured-endpoint-unavailable",
|
||
"endpoint-target-unconfigured",
|
||
]) {
|
||
assert.equal(readOnlyFailureShowsNetworkUnavailable(reasonCode), true);
|
||
}
|
||
for (const reasonCode of [
|
||
"connection-verify-device-not-rediscovered",
|
||
"connection-verify-status-read-invalid",
|
||
"device-identity-pin-mismatch",
|
||
"connection-verify-discovery-generation-conflict",
|
||
"connection-verify-lease-changed",
|
||
"connection-reconfiguration-revision-conflict",
|
||
"connection-verify-busy",
|
||
null,
|
||
]) {
|
||
assert.equal(readOnlyFailureShowsNetworkUnavailable(reasonCode), false);
|
||
}
|
||
});
|
||
|
||
test("a BLE-proven missing Bridge address requires network setup across later retries", () => {
|
||
const state = {
|
||
connection_verification: {
|
||
status: "unreachable",
|
||
reason_code: "connection-verify-address-unavailable",
|
||
},
|
||
last_operation: {
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: { code: "connection-verify-device-not-rediscovered" },
|
||
},
|
||
};
|
||
assert.equal(savedBridgeRequiresNetworkSetup(state), true);
|
||
assert.equal(savedBridgeRequiresNetworkSetup({
|
||
last_operation: {
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: { code: "connection-verify-address-unavailable" },
|
||
},
|
||
}), true);
|
||
assert.equal(savedBridgeRequiresNetworkSetup({
|
||
connection_verification: {
|
||
status: "unreachable",
|
||
reason_code: "connection-verify-device-not-rediscovered",
|
||
},
|
||
}), false);
|
||
});
|
||
|
||
test("pre-write candidate loss is classified stale without granting UI continuation", () => {
|
||
for (const reasonCode of [
|
||
"BleakDeviceNotFoundError",
|
||
"network-provision-candidate-not-fresh",
|
||
"network-provision-candidate-changed",
|
||
"network-provision-discovery-generation-conflict",
|
||
]) {
|
||
assert.equal(provisioningFailureRequiresFreshCandidate(reasonCode), true);
|
||
}
|
||
for (const reasonCode of [
|
||
"BleakGATTProtocolError",
|
||
"network-provision-target-not-distinguishable-from-baseline",
|
||
"network-provision-lifecycle-busy",
|
||
"host-wifi-operation-timeout",
|
||
null,
|
||
]) {
|
||
assert.equal(provisioningFailureRequiresFreshCandidate(reasonCode), false);
|
||
}
|
||
});
|
||
|
||
test("stale old-device reachability cannot own a fresh selection outcome", () => {
|
||
const staleOldDeviceProjection = {
|
||
connection_verification: {
|
||
status: "endpoint-unreachable",
|
||
lease_state: "configured-unverified",
|
||
network_reachability: "unreachable",
|
||
transport_ref: "ble-k1-old",
|
||
},
|
||
};
|
||
|
||
// The global projection is deliberately stale and unavailable, but the
|
||
// correlated fresh-device operation failed identity validation. The normal
|
||
// one-intent UI renders this only as a stale/safety outcome; it never scans,
|
||
// verifies or submits automatically.
|
||
assert.equal(
|
||
readOnlyObservationShowsNetworkUnavailable(staleOldDeviceProjection),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
readOnlyFailureShowsNetworkUnavailable("connection-verify-status-read-invalid"),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
readOnlyFailureShowsNetworkUnavailable("device-identity-pin-mismatch"),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
readOnlyFailureShowsNetworkUnavailable("connection-reconfiguration-revision-conflict"),
|
||
false,
|
||
);
|
||
assert.equal(
|
||
readOnlyFailureShowsNetworkUnavailable("BleakGATTProtocolError"),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("Quick Connect does not expose Bridge reconfiguration actions", () => {
|
||
const state = runtimeState();
|
||
state.phase = "connected";
|
||
state.source_mode = "idle";
|
||
state.acquisition = null;
|
||
state.connection_mode = "quick-connect";
|
||
state.configured_connection_mode = "quick-connect";
|
||
state.active_connection_mode = "quick-connect";
|
||
state.desired_connection_mode = "quick-connect";
|
||
state.connection_supervisor = supervisor({ control: true, data: false });
|
||
state.connection_supervisor.intent.requested_mode = "quick-connect";
|
||
state.connection_supervisor.observed.device_network.connection_mode = "quick-connect";
|
||
state.connection_supervisor.observed.device_identity.connection_mode = "quick-connect";
|
||
state.connection_supervisor.lease.connection_mode = "quick-connect";
|
||
state.connection_lifecycle = connectionLifecycle({ mode: "quick-connect" });
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: [],
|
||
actions: {},
|
||
};
|
||
|
||
const markup = renderProvisioning({
|
||
controller: provisioningController(state),
|
||
desiredMode: "quick-connect",
|
||
});
|
||
assert.doesNotMatch(markup, /Выбрать другое устройство|Изменить сеть/);
|
||
});
|
||
|
||
test("a verify without current click authority does not invent network step 02", () => {
|
||
const state = durableTopologyState();
|
||
state.semantic_topology_store = {
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: null,
|
||
};
|
||
state.connection_supervisor = supervisor({ control: false, data: false });
|
||
state.connection_lifecycle = connectionLifecycle({ control: false });
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: [],
|
||
actions: {},
|
||
};
|
||
|
||
const markup = renderProvisioning({
|
||
controller: {
|
||
...provisioningController(state),
|
||
pendingAction: "verify",
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
isConnectionActionAuthorityCurrent: () => false,
|
||
},
|
||
desiredMode: "bridge",
|
||
});
|
||
|
||
assert.match(markup, /<span>01<\/span>/);
|
||
assert.doesNotMatch(markup, /<span>02<\/span>|<span>03<\/span>/);
|
||
assert.doesNotMatch(markup, /aria-busy="true"/);
|
||
assert.equal((markup.match(/class="nodedc-activity-indicator"/g) ?? []).length, 0);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
|
||
|
||
|
||
test("a blocked local-only device escape explains backend safety reasons", () => {
|
||
const state = durableTopologyState();
|
||
state.physical_command = {
|
||
operator_retirement: {
|
||
allowed: false,
|
||
reason_codes: ["physical-command-retirement-operation-conflict"],
|
||
expected_operation_id: "old-stop-operation",
|
||
expected_revision: 280,
|
||
expected_transport_ref: "ble-k1-001",
|
||
physical_outcome: "unknown",
|
||
device_io_performed: false,
|
||
automatic_retry: false,
|
||
},
|
||
};
|
||
assert.match(
|
||
physicalRetirementGuidance(state),
|
||
/завершается другая операция с физическим состоянием устройства/,
|
||
);
|
||
|
||
for (const [reasonCode, expectedCopy] of [
|
||
["physical-command-retirement-state-unsafe", /Состояние предыдущей физической команды изменилось/],
|
||
["physical-command-retirement-not-required", /больше не удерживает выбор устройства/],
|
||
["physical-command-target-retired", /уже выведено из текущего контура/],
|
||
["acquisition-active", /Сканирование ещё активно/],
|
||
["control-session-state-unsafe", /Управляющая сессия устройства ещё не завершена/],
|
||
["ble-runtime-busy", /Bluetooth занят другой операцией устройства/],
|
||
["ble-runtime-cleanup-pending", /Bluetooth ещё завершает предыдущую операцию/],
|
||
]) {
|
||
state.physical_command.operator_retirement.reason_codes = [reasonCode];
|
||
assert.match(physicalRetirementGuidance(state), expectedCopy);
|
||
}
|
||
});
|
||
|
||
test("a blocked exact retired-device check explains safe next actions", () => {
|
||
const state = durableTopologyState();
|
||
state.physical_command = {
|
||
operator_reconciliation_reopen: {
|
||
allowed: false,
|
||
reason_codes: ["physical-command-reconciliation-reopen-target-not-observed"],
|
||
expected_revision: 281,
|
||
expected_retirement_id: "retirement-old-stop",
|
||
expected_transport_ref: "ble-k1-001",
|
||
expected_discovery_generation: 13,
|
||
expected_desired_mode: "bridge",
|
||
expected_desired_mode_revision: 0,
|
||
device_io_performed: false,
|
||
automatic_retry: false,
|
||
},
|
||
};
|
||
assert.match(physicalReopenGuidance(state), /не найдено в последнем Bluetooth-поиске/);
|
||
state.physical_command.operator_reconciliation_reopen.reason_codes = [
|
||
"physical-command-reconciliation-reopen-target-not-connectable",
|
||
];
|
||
assert.match(physicalReopenGuidance(state), /не принимает Bluetooth-подключение/);
|
||
state.physical_command.operator_reconciliation_reopen.reason_codes = [
|
||
"physical-command-reconciliation-reopen-candidate-ambiguous",
|
||
];
|
||
assert.match(physicalReopenGuidance(state), /не подтвердил один точный экземпляр/);
|
||
state.physical_command.operator_reconciliation_reopen.reason_codes = [
|
||
"device-calibration-read-active",
|
||
];
|
||
assert.match(physicalReopenGuidance(state), /читается калибровка/);
|
||
});
|
||
|
||
test("fresh Scan keeps retired audit rows selectable without recovery I/O", () => {
|
||
const state = durableTopologyState();
|
||
state.current_device_recovery = {
|
||
transport_ref: "ble-k1-001",
|
||
connection_mode: "bridge",
|
||
handle_retained: false,
|
||
advertised_now: false,
|
||
};
|
||
state.physical_command = {
|
||
status: "resolved",
|
||
reason_code: null,
|
||
requires_reconciliation: false,
|
||
automatic_replay_allowed: false,
|
||
normal_session_recovery_supported: false,
|
||
runtime_bound: false,
|
||
reconciliation_ready: false,
|
||
record: {
|
||
revision: 281,
|
||
operation_id: "old-stop-operation",
|
||
action: "stop",
|
||
stage: "resolved",
|
||
resolution: "operator-retired-outcome-unknown",
|
||
connection: { transport_ref: "ble-k1-001" },
|
||
operator_retirements: [{ retired_transport_ref: "ble-k1-001" }],
|
||
},
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: {
|
||
retained_context_is_presence: false,
|
||
retired_transport_refs: ["ble-k1-001"],
|
||
},
|
||
allowed_actions: ["scan-ble", "provision-fresh-device"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
state.connection_attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: "historical-network-attempt-before-retirement",
|
||
connection_mode: "bridge",
|
||
status: "failed",
|
||
phase: "network_applied",
|
||
control_state: "unknown",
|
||
stage: "host-route-and-control-endpoint",
|
||
public_error_code: "control-bootstrap-failed",
|
||
side_effect_status: "network-applied",
|
||
safe_next_action: "scan-select-connect",
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: OBSERVED_AT,
|
||
timeline: [],
|
||
};
|
||
state.devices = [
|
||
{
|
||
device_id: "ble-k1-001",
|
||
name: "Retired old device",
|
||
rssi: -39,
|
||
connectable: true,
|
||
likely_k1: true,
|
||
},
|
||
{
|
||
device_id: "ble-k1-new",
|
||
name: "Replacement device",
|
||
rssi: -52,
|
||
connectable: true,
|
||
likely_k1: true,
|
||
},
|
||
];
|
||
|
||
assert.equal(trustedConnectionBinding(state), null);
|
||
assert.equal(
|
||
connectionAttemptOwnsAppliedNetworkRecovery(state.connection_attempt),
|
||
false,
|
||
);
|
||
const markup = renderProvisioningAfterSearch({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
});
|
||
assert.match(markup, /Результатов: 2/);
|
||
assert.match(markup, /Повторить поиск Bluetooth/);
|
||
assert.doesNotMatch(markup, /old-stop-operation/);
|
||
const priorAction = deviceRowActionButton(markup, "ble-k1-001");
|
||
assert.match(priorAction, />Выбрать<\/button>/);
|
||
assert.doesNotMatch(priorAction, /\bdisabled(?:=|\s|>)/);
|
||
const replacementAction = deviceRowActionButton(markup, "ble-k1-new");
|
||
assert.match(replacementAction, />Выбрать<\/button>/);
|
||
assert.doesNotMatch(replacementAction, /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(
|
||
buttonMarkupWithText(markup, "Подключить новый K1").length,
|
||
0,
|
||
);
|
||
assert.doesNotMatch(markup, /<button[^>]*disabled[^>]*>Недоступно<\/button>/);
|
||
assert.doesNotMatch(
|
||
markup,
|
||
/Попытка настройки завершена|Проверить подключение без изменения сети|Управление не подтверждено/,
|
||
);
|
||
assert.doesNotMatch(markup, /Название сети Wi‑Fi|Пароль Wi‑Fi|>Применить</);
|
||
assertCanonicalConnectionCopy(markup);
|
||
|
||
const onlyPrior = structuredClone(state);
|
||
onlyPrior.devices = [state.devices[0]];
|
||
const onlyPriorMarkup = renderProvisioningAfterSearch({
|
||
controller: provisioningController(onlyPrior),
|
||
desiredMode: "bridge",
|
||
});
|
||
assert.equal(
|
||
buttonMarkupWithText(onlyPriorMarkup, "Подключить новый K1").length,
|
||
0,
|
||
);
|
||
assert.doesNotMatch(
|
||
onlyPriorMarkup,
|
||
/Подходящих K1 не найдено\. Повторите поиск\./,
|
||
);
|
||
const onlyPriorAction = deviceRowActionButton(onlyPriorMarkup, "ble-k1-001");
|
||
assert.match(onlyPriorAction, />Выбрать<\/button>/);
|
||
assert.doesNotMatch(onlyPriorAction, /\bdisabled(?:=|\s|>)/);
|
||
assert.match(onlyPriorMarkup, /Результатов: 1/);
|
||
assert.doesNotMatch(onlyPriorMarkup, />Недоступно|Переподключиться/);
|
||
|
||
// Historical audit data is not an active deny once the authoritative policy
|
||
// projection removes the reference. The same advertised row then behaves as
|
||
// an ordinary fresh result, without a saved/reopen branch in the UI.
|
||
state.connection_policy.facts.retired_transport_refs = [];
|
||
state.connection_lifecycle = connectionLifecycle({ control: false });
|
||
state.connection_supervisor = supervisor({ control: false, data: false });
|
||
state.semantic_topology_store = {
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: null,
|
||
};
|
||
state.current_device_recovery = null;
|
||
const historicalAuditMarkup = renderProvisioningAfterSearch({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
});
|
||
const historicallyRetiredAction = deviceRowActionButton(
|
||
historicalAuditMarkup,
|
||
"ble-k1-001",
|
||
);
|
||
assert.match(historicallyRetiredAction, />Выбрать<\/button>/);
|
||
assert.doesNotMatch(
|
||
historicallyRetiredAction,
|
||
/\bdisabled(?:=|\s|>)/,
|
||
);
|
||
assertCanonicalConnectionCopy(historicalAuditMarkup);
|
||
});
|
||
|
||
test("only an active network operation owns an applied-attempt screen", () => {
|
||
const attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: "attempt-network-recovery-ownership",
|
||
connection_mode: "bridge",
|
||
status: "failed",
|
||
phase: "network_applied",
|
||
control_state: "control_not_ready",
|
||
stage: "host-route-and-control-endpoint",
|
||
public_error_code: "control-bootstrap-failed",
|
||
side_effect_status: "network-applied",
|
||
safe_next_action: "verify-control-read-only",
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: OBSERVED_AT,
|
||
timeline: [],
|
||
};
|
||
for (const safeNextAction of [
|
||
"continue-with-control-verification",
|
||
"verify-control-read-only",
|
||
"manual-recovery-required",
|
||
]) {
|
||
assert.equal(
|
||
connectionAttemptOwnsAppliedNetworkRecovery({
|
||
...attempt,
|
||
safe_next_action: safeNextAction,
|
||
}),
|
||
false,
|
||
safeNextAction,
|
||
);
|
||
}
|
||
for (const safeNextAction of [
|
||
"scan-select-connect",
|
||
"stop-local-receiver",
|
||
"retire-unavailable-physical-target",
|
||
"start-acquisition",
|
||
]) {
|
||
assert.equal(
|
||
connectionAttemptOwnsAppliedNetworkRecovery({
|
||
...attempt,
|
||
safe_next_action: safeNextAction,
|
||
}),
|
||
false,
|
||
safeNextAction,
|
||
);
|
||
}
|
||
assert.equal(
|
||
connectionAttemptOwnsAppliedNetworkRecovery({
|
||
...attempt,
|
||
status: "running",
|
||
safe_next_action: "wait-for-current-attempt",
|
||
}),
|
||
true,
|
||
);
|
||
assert.equal(
|
||
connectionAttemptOwnsAppliedNetworkRecovery({
|
||
...attempt,
|
||
control_state: "ready",
|
||
}),
|
||
false,
|
||
);
|
||
});
|
||
|
||
test("an exact prior CoreBluetooth UUID remains a normal fresh selection across casing", () => {
|
||
const state = retiredPhysicalReopenReadyState({
|
||
advertisedRef: "f89438fa-55ed-85ad-eed7-734ac84746d8",
|
||
backendRef: "F89438FA-55ED-85AD-EED7-734AC84746D8",
|
||
});
|
||
|
||
assert.equal(
|
||
transportRefEquivalenceKey(" F89438FA-55ED-85AD-EED7-734AC84746D8 "),
|
||
"f89438fa-55ed-85ad-eed7-734ac84746d8",
|
||
);
|
||
assert.equal(trustedConnectionBinding(state), null);
|
||
const markup = renderProvisioningAfterSearch({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
});
|
||
const priorDeviceAction = deviceRowActionButton(
|
||
markup,
|
||
"f89438fa-55ed-85ad-eed7-734ac84746d8",
|
||
);
|
||
assert.match(priorDeviceAction, />Выбрать<\/button>/);
|
||
assert.doesNotMatch(priorDeviceAction, /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(buttonMarkupWithText(markup, "Переподключиться").length, 0);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
test("an exact fresh prior row exposes one enabled Select without reconnect", () => {
|
||
const state = retiredPhysicalReopenReadyState();
|
||
assert.ok(retiredPhysicalReopenAuthority(
|
||
state,
|
||
state.devices[0].device_id,
|
||
"bridge",
|
||
));
|
||
const markup = renderProvisioningAfterSearch({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
});
|
||
const selectActions = buttonMarkupWithText(markup, "Выбрать");
|
||
assert.equal(selectActions.length, 1);
|
||
assert.doesNotMatch(selectActions[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(buttonMarkupWithText(markup, "Переподключиться").length, 0);
|
||
assert.doesNotMatch(markup, /Вернуть прежний K1 и проверить/);
|
||
assert.doesNotMatch(markup, /Название общей сети Wi‑Fi|Пароль Wi‑Fi|>Применить</);
|
||
});
|
||
|
||
test("post-reset Scan selects the exact prior UUID locally and opens network fields", async () => {
|
||
const initialState = retiredPhysicalReopenReadyState();
|
||
initialState.devices.push({
|
||
device_id: "A1619D95-C352-1069-D430-5FB0BC13F7F9",
|
||
name: "dcCONSTRUCTIONS",
|
||
rssi: -55,
|
||
connectable: true,
|
||
likely_k1: false,
|
||
});
|
||
initialState.connection_scenario_reset = {
|
||
reset_id: "reset-before-retired-reconnect",
|
||
request_revision: 6,
|
||
revision: 7,
|
||
desired_mode: "bridge",
|
||
active: false,
|
||
settled_by_discovery_generation: initialState.ble_discovery_generation,
|
||
local_session_closed: true,
|
||
previous_device_may_continue_scanning: false,
|
||
physical_disposition: "retired",
|
||
network_disposition: null,
|
||
device_command_performed: false,
|
||
network_write_performed: false,
|
||
automatic_scan: false,
|
||
operation_sequence: 21,
|
||
};
|
||
let currentState = initialState;
|
||
let scanCalls = 0;
|
||
let reopenCalls = 0;
|
||
let verifyCalls = 0;
|
||
let connectCalls = 0;
|
||
let resetCalls = 0;
|
||
let prepareCalls = 0;
|
||
let markScanDispatched;
|
||
const scanDispatched = new Promise((resolve) => {
|
||
markScanDispatched = resolve;
|
||
});
|
||
const controller = {
|
||
...provisioningController(initialState),
|
||
scanWithResult: async () => {
|
||
scanCalls += 1;
|
||
markScanDispatched();
|
||
return {
|
||
succeeded: true,
|
||
snapshotRuntimeId: currentState.snapshot_runtime_id,
|
||
discoveryGeneration: currentState.ble_discovery_generation,
|
||
transportRefs: currentState.devices.map((device) => device.device_id),
|
||
};
|
||
},
|
||
reopenRetiredPhysicalReconciliation: async () => {
|
||
reopenCalls += 1;
|
||
return { succeeded: false, observedState: currentState };
|
||
},
|
||
verifyConnection: async () => {
|
||
verifyCalls += 1;
|
||
return {
|
||
succeeded: false,
|
||
reconciliationCompleted: false,
|
||
observedState: currentState,
|
||
};
|
||
},
|
||
connect: async () => {
|
||
connectCalls += 1;
|
||
return {
|
||
succeeded: false,
|
||
networkIntentCompleted: false,
|
||
intentDisposition: "retain",
|
||
acceptedSessionKey: null,
|
||
};
|
||
},
|
||
selectConnectionMode: async () => {
|
||
resetCalls += 1;
|
||
return true;
|
||
},
|
||
prepareConnectionReconfigurationWithResult: async () => {
|
||
prepareCalls += 1;
|
||
return { succeeded: false, observedState: currentState };
|
||
},
|
||
getCurrentState: () => currentState,
|
||
getConnectionRecoveryObservationTarget: () =>
|
||
recommendedConnectionRecoveryObservationTarget(currentState),
|
||
getConnectionActionAuthority: (mode) =>
|
||
connectionActionAuthoritySnapshot(currentState, mode),
|
||
isSnapshotRuntimeCurrent: (runtimeId) =>
|
||
runtimeId === currentState.snapshot_runtime_id,
|
||
isConnectionActionAuthorityCurrent: (authority) =>
|
||
connectionActionAuthorityMatches(
|
||
authority,
|
||
connectionActionAuthoritySnapshot(
|
||
currentState,
|
||
authority.connectionMode,
|
||
),
|
||
),
|
||
isConnectionPolicyActionAllowedCurrent: (action) =>
|
||
currentState.connection_policy.allowed_actions.includes(action),
|
||
};
|
||
const props = () => ({
|
||
controller: { ...controller, state: currentState },
|
||
desiredMode: "bridge",
|
||
});
|
||
const harness = createStatefulProvisioningHarness(props());
|
||
|
||
try {
|
||
let tree = harness.render(props());
|
||
harness.flushEffects();
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
const scan = actionByLabel(tree, "Найти по Bluetooth");
|
||
assert.ok(scan);
|
||
scan.props.onClick();
|
||
await Promise.race([
|
||
scanDispatched,
|
||
new Promise((_, reject) => setTimeout(
|
||
() => reject(new Error("post-reset Scan did not dispatch")),
|
||
100,
|
||
)),
|
||
]);
|
||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
const priorRow = elementByProp(tree, "actionLabel", "Выбрать");
|
||
assert.ok(priorRow);
|
||
assert.equal(priorRow.props.device.device_id, initialState.devices[0].device_id);
|
||
assert.equal(priorRow.props.selectionDisabled, false);
|
||
assert.equal(actionByLabel(tree, "Переподключиться"), null);
|
||
priorRow.props.onSelect();
|
||
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
tree = harness.render(props());
|
||
const selectedMarkup = renderToStaticMarkup(tree);
|
||
assert.equal(scanCalls, 1);
|
||
assert.equal(reopenCalls, 0);
|
||
assert.equal(verifyCalls, 0);
|
||
assert.equal(connectCalls, 0);
|
||
assert.equal(resetCalls, 0);
|
||
assert.equal(prepareCalls, 0);
|
||
assert.match(selectedMarkup, /<span>02<\/span>/);
|
||
assert.match(selectedMarkup, /Название общей сети Wi‑Fi/);
|
||
assert.match(selectedMarkup, /Пароль Wi‑Fi/);
|
||
assert.doesNotMatch(selectedMarkup, /Переподключиться/);
|
||
assert.equal(
|
||
harness.stateValue(emptyProvisioningAttemptPresentation),
|
||
null,
|
||
"local selection must not manufacture a backend Apply attempt",
|
||
);
|
||
|
||
const passwordField = elementByProp(tree, "label", "Пароль Wi‑Fi");
|
||
assert.ok(passwordField);
|
||
assert.equal(passwordField.props.type, "password");
|
||
passwordField.props.onChange({ target: { value: "test-only-password" } });
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
const showPassword = elementByProp(tree, "label", "Показать пароль");
|
||
assert.ok(showPassword);
|
||
assert.equal(showPassword.props.disabled, false);
|
||
showPassword.props.onClick();
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
assert.equal(
|
||
elementByProp(tree, "label", "Пароль Wi‑Fi").props.type,
|
||
"text",
|
||
);
|
||
assert.ok(elementByProp(tree, "label", "Скрыть пароль"));
|
||
|
||
const chooseAnother = actionByLabel(tree, "Выбрать другое");
|
||
assert.ok(chooseAnother);
|
||
chooseAnother.props.onClick();
|
||
|
||
tree = harness.render(props());
|
||
harness.flushEffects();
|
||
tree = harness.render(props());
|
||
const returnedMarkup = renderToStaticMarkup(tree);
|
||
const returnedRows = buttonMarkupWithText(returnedMarkup, "Выбрать");
|
||
assert.equal(returnedRows.length, initialState.devices.length);
|
||
assert.ok(returnedRows.every((row) => !/\bdisabled(?:=|\s|>)/.test(row)));
|
||
assert.match(returnedMarkup, /Повторить поиск Bluetooth/);
|
||
assert.doesNotMatch(returnedMarkup, />Найти по Bluetooth</);
|
||
assert.doesNotMatch(
|
||
returnedMarkup,
|
||
/Название общей сети Wi‑Fi|Пароль Wi‑Fi|<span>02<\/span>/,
|
||
);
|
||
assert.equal(scanCalls, 1);
|
||
assert.equal(reopenCalls, 0);
|
||
assert.equal(verifyCalls, 0);
|
||
assert.equal(connectCalls, 0);
|
||
assert.equal(resetCalls, 0);
|
||
assert.equal(prepareCalls, 0);
|
||
} finally {
|
||
harness.dispose();
|
||
}
|
||
});
|
||
|
||
test("a rendered prior-device selection never dispatches reopen or Verify", async () => {
|
||
const initialState = retiredPhysicalReopenReadyState();
|
||
let currentState = initialState;
|
||
let reopenCalls = 0;
|
||
let verifyCalls = 0;
|
||
let reopenedRequest = null;
|
||
let verifiedRequest = null;
|
||
let verifiedOptions = null;
|
||
let markVerifyDispatched;
|
||
const verifyDispatched = new Promise((resolve) => {
|
||
markVerifyDispatched = resolve;
|
||
});
|
||
|
||
const controller = {
|
||
...provisioningController(initialState),
|
||
getCurrentState: () => currentState,
|
||
getConnectionRecoveryObservationTarget: () =>
|
||
recommendedConnectionRecoveryObservationTarget(currentState),
|
||
getConnectionActionAuthority: (mode) =>
|
||
connectionActionAuthoritySnapshot(currentState, mode),
|
||
isSnapshotRuntimeCurrent: (runtimeId) =>
|
||
runtimeId === currentState.snapshot_runtime_id,
|
||
reopenRetiredPhysicalReconciliation: async (request, runtimeId) => {
|
||
reopenCalls += 1;
|
||
reopenedRequest = request;
|
||
assert.equal(runtimeId, initialState.snapshot_runtime_id);
|
||
const committed = reopenedPhysicalState({
|
||
reopeningId: request.reopening_id,
|
||
discoveryGeneration: request.expected_discovery_generation,
|
||
});
|
||
committed.connection_policy.recommended_action = "select-connection-intent";
|
||
committed.connection_policy.allowed_actions = [
|
||
"observe-fresh-device-network",
|
||
"observe-configured-device-network",
|
||
"scan-ble",
|
||
];
|
||
committed.connection_policy.actions["observe-configured-device-network"] = {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: request.expected_transport_ref,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
};
|
||
currentState = committed;
|
||
assert.equal(
|
||
recommendedConnectionRecoveryObservationTarget(currentState).source,
|
||
"durable-configured-state",
|
||
"the generic selector must reproduce the competing configured target",
|
||
);
|
||
assert.equal(
|
||
readOnlyConnectionObservationTarget(currentState).source,
|
||
"fresh-scan",
|
||
);
|
||
return { succeeded: true, observedState: currentState };
|
||
},
|
||
verifyConnection: async (request, options) => {
|
||
verifyCalls += 1;
|
||
verifiedRequest = request;
|
||
verifiedOptions = options;
|
||
markVerifyDispatched();
|
||
return {
|
||
succeeded: true,
|
||
reconciliationCompleted: true,
|
||
observedState: currentState,
|
||
};
|
||
},
|
||
};
|
||
const tree = captureProvisioningTreeAfterSearch({
|
||
controller,
|
||
desiredMode: "bridge",
|
||
});
|
||
const priorDeviceRow = elementByProp(
|
||
tree,
|
||
"actionLabel",
|
||
"Выбрать",
|
||
);
|
||
assert.ok(priorDeviceRow);
|
||
assert.equal(typeof priorDeviceRow.props.onSelect, "function");
|
||
assert.equal(actionByLabel(tree, "Переподключиться"), null);
|
||
|
||
priorDeviceRow.props.onSelect();
|
||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||
|
||
assert.equal(reopenCalls, 0);
|
||
assert.equal(verifyCalls, 0);
|
||
assert.equal(reopenedRequest, null);
|
||
assert.equal(verifiedRequest, null);
|
||
assert.equal(verifiedOptions, null);
|
||
});
|
||
|
||
test("retired physical reopen proof uses the backend target, not the visible row", () => {
|
||
const request = {
|
||
reopening_id: "reopening-returned-k1",
|
||
expected_revision: 281,
|
||
expected_retirement_id: "retirement-old-stop",
|
||
expected_transport_ref: "F89438FA-55ED-85AD-EED7-734AC84746D8",
|
||
expected_discovery_generation: 13,
|
||
expected_desired_mode: "bridge",
|
||
expected_desired_mode_revision: 7,
|
||
operator_confirmed: true,
|
||
reason: "device-returned-for-explicit-reconciliation",
|
||
};
|
||
const state = reopenedPhysicalState();
|
||
const target = recommendedConnectionRecoveryObservationTarget(state);
|
||
const authority = connectionActionAuthoritySnapshot(state, "bridge");
|
||
const context = retiredPhysicalReopenVerificationContext(
|
||
state,
|
||
request,
|
||
"runtime-reopened-k1",
|
||
target,
|
||
authority,
|
||
);
|
||
assert.ok(context);
|
||
assert.equal(context.target.deviceId, request.expected_transport_ref);
|
||
assert.equal(context.target.serverBound, true);
|
||
assert.equal(
|
||
context.target.deviceId,
|
||
state.connection_policy.actions["observe-fresh-device-network"]
|
||
.required_transport_ref,
|
||
);
|
||
assert.notEqual(context.target.deviceId, state.devices[0].device_id);
|
||
assert.equal(
|
||
transportRefEquivalenceKey(context.target.deviceId),
|
||
transportRefEquivalenceKey(state.devices[0].device_id),
|
||
);
|
||
|
||
const wrongTarget = {
|
||
...target,
|
||
deviceId: "visible-row-but-not-server-target",
|
||
};
|
||
assert.equal(retiredPhysicalReopenVerificationContext(
|
||
state,
|
||
request,
|
||
"runtime-reopened-k1",
|
||
wrongTarget,
|
||
authority,
|
||
), null);
|
||
});
|
||
|
||
test("exact lost reopen response continues once to read-only Verify", async () => {
|
||
const request = {
|
||
reopening_id: "reopening-returned-k1",
|
||
expected_revision: 281,
|
||
expected_retirement_id: "retirement-old-stop",
|
||
expected_transport_ref: "F89438FA-55ED-85AD-EED7-734AC84746D8",
|
||
expected_discovery_generation: 13,
|
||
expected_desired_mode: "bridge",
|
||
expected_desired_mode_revision: 7,
|
||
operator_confirmed: true,
|
||
reason: "device-returned-for-explicit-reconciliation",
|
||
};
|
||
const committed = reopenedPhysicalState();
|
||
let verifyCalls = 0;
|
||
let verifiedTarget = null;
|
||
const result = await dispatchRetiredPhysicalReconciliationForCurrentRuntime({
|
||
snapshotRuntimeId: "runtime-reopened-k1",
|
||
request,
|
||
isSnapshotRuntimeCurrent: (runtimeId) => runtimeId === "runtime-reopened-k1",
|
||
reopen: async () => ({ succeeded: false, observedState: committed }),
|
||
getCurrentState: () => committed,
|
||
getCurrentTarget: () => recommendedConnectionRecoveryObservationTarget(committed),
|
||
getCurrentAuthority: (mode) => connectionActionAuthoritySnapshot(committed, mode),
|
||
expectedAuthority: connectionActionAuthoritySnapshot(committed, "bridge"),
|
||
verify: async (context, runtimeId) => {
|
||
verifyCalls += 1;
|
||
verifiedTarget = context.target;
|
||
assert.equal(runtimeId, "runtime-reopened-k1");
|
||
return { succeeded: true };
|
||
},
|
||
});
|
||
assert.equal(result.reopenDispatched, true);
|
||
assert.equal(result.reopenResult.succeeded, false);
|
||
assert.equal(result.verifyDispatched, true);
|
||
assert.equal(verifyCalls, 1);
|
||
assert.equal(verifiedTarget.deviceId, request.expected_transport_ref);
|
||
});
|
||
|
||
test("retired combined recovery keeps the exact fresh target when configured observation is also allowed", async () => {
|
||
const request = {
|
||
reopening_id: "reopening-combined-live",
|
||
expected_revision: 281,
|
||
expected_retirement_id: "retirement-old-stop",
|
||
expected_transport_ref: "F89438FA-55ED-85AD-EED7-734AC84746D8",
|
||
expected_discovery_generation: 13,
|
||
expected_desired_mode: "bridge",
|
||
expected_desired_mode_revision: 7,
|
||
operator_confirmed: true,
|
||
reason: "device-returned-for-explicit-reconciliation",
|
||
};
|
||
const committed = reopenedPhysicalState({
|
||
reopeningId: request.reopening_id,
|
||
});
|
||
committed.connection_policy.recommended_action = "select-connection-intent";
|
||
committed.connection_policy.allowed_actions = [
|
||
"observe-fresh-device-network",
|
||
"observe-configured-device-network",
|
||
"scan-ble",
|
||
];
|
||
committed.connection_policy.actions["observe-configured-device-network"] = {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: request.expected_transport_ref,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
};
|
||
|
||
const genericTarget = recommendedConnectionRecoveryObservationTarget(committed);
|
||
const exactFreshTarget = readOnlyConnectionObservationTarget(committed);
|
||
assert.equal(genericTarget.action, "observe-configured-device-network");
|
||
assert.equal(exactFreshTarget.action, "observe-fresh-device-network");
|
||
assert.equal(exactFreshTarget.source, "fresh-scan");
|
||
assert.equal(exactFreshTarget.expectedDiscoveryGeneration, 13);
|
||
|
||
let verifyCalls = 0;
|
||
const result = await dispatchRetiredPhysicalReconciliationForCurrentRuntime({
|
||
snapshotRuntimeId: committed.snapshot_runtime_id,
|
||
request,
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
reopen: async () => ({ succeeded: true, observedState: committed }),
|
||
getCurrentState: () => committed,
|
||
getCurrentTarget: () => readOnlyConnectionObservationTarget(committed),
|
||
getCurrentAuthority: (mode) => connectionActionAuthoritySnapshot(committed, mode),
|
||
expectedAuthority: connectionActionAuthoritySnapshot(committed, "bridge"),
|
||
verify: async ({ target }) => {
|
||
verifyCalls += 1;
|
||
assert.equal(target.action, "observe-fresh-device-network");
|
||
assert.equal(target.source, "fresh-scan");
|
||
assert.equal(target.deviceId, request.expected_transport_ref);
|
||
return { succeeded: true };
|
||
},
|
||
});
|
||
assert.equal(result.verifyDispatched, true);
|
||
assert.equal(verifyCalls, 1);
|
||
});
|
||
|
||
test("runtime replacement and same-runtime re-retirement suppress post-reopen Verify", async () => {
|
||
const request = {
|
||
reopening_id: "reopening-returned-k1",
|
||
expected_revision: 281,
|
||
expected_retirement_id: "retirement-old-stop",
|
||
expected_transport_ref: "F89438FA-55ED-85AD-EED7-734AC84746D8",
|
||
expected_discovery_generation: 13,
|
||
expected_desired_mode: "bridge",
|
||
expected_desired_mode_revision: 7,
|
||
operator_confirmed: true,
|
||
reason: "device-returned-for-explicit-reconciliation",
|
||
};
|
||
const committed = reopenedPhysicalState();
|
||
let runtimeId = "runtime-B";
|
||
let reopenCalls = 0;
|
||
let verifyCalls = 0;
|
||
const staleBeforeDispatch = await dispatchRetiredPhysicalReconciliationForCurrentRuntime({
|
||
snapshotRuntimeId: "runtime-A",
|
||
request,
|
||
isSnapshotRuntimeCurrent: (expected) => expected === runtimeId,
|
||
reopen: async () => {
|
||
reopenCalls += 1;
|
||
return { succeeded: true, observedState: committed };
|
||
},
|
||
getCurrentState: () => committed,
|
||
getCurrentTarget: () => recommendedConnectionRecoveryObservationTarget(committed),
|
||
getCurrentAuthority: (mode) => connectionActionAuthoritySnapshot(committed, mode),
|
||
expectedAuthority: connectionActionAuthoritySnapshot(committed, "bridge"),
|
||
verify: async () => {
|
||
verifyCalls += 1;
|
||
return { succeeded: true };
|
||
},
|
||
});
|
||
assert.equal(staleBeforeDispatch.reopenDispatched, false);
|
||
assert.equal(reopenCalls, 0);
|
||
assert.equal(verifyCalls, 0);
|
||
|
||
runtimeId = "runtime-reopened-k1";
|
||
const reretired = retiredPhysicalReopenReadyState();
|
||
let currentState = committed;
|
||
const changedAfterSuccess = await dispatchRetiredPhysicalReconciliationForCurrentRuntime({
|
||
snapshotRuntimeId: runtimeId,
|
||
request,
|
||
isSnapshotRuntimeCurrent: (expected) => expected === runtimeId,
|
||
reopen: async () => {
|
||
currentState = reretired;
|
||
return { succeeded: true, observedState: committed };
|
||
},
|
||
getCurrentState: () => currentState,
|
||
getCurrentTarget: () => recommendedConnectionRecoveryObservationTarget(currentState),
|
||
getCurrentAuthority: (mode) => connectionActionAuthoritySnapshot(currentState, mode),
|
||
expectedAuthority: connectionActionAuthoritySnapshot(committed, "bridge"),
|
||
verify: async () => {
|
||
verifyCalls += 1;
|
||
return { succeeded: true };
|
||
},
|
||
});
|
||
assert.equal(changedAfterSuccess.reopenDispatched, true);
|
||
assert.equal(changedAfterSuccess.verifyDispatched, false);
|
||
assert.equal(verifyCalls, 0);
|
||
});
|
||
|
||
test("retired reopen click and settlement retain the complete authority fence", () => {
|
||
const rendered = retiredPhysicalReopenReadyState();
|
||
rendered.connection_reconfiguration = connectionReconfiguration();
|
||
rendered.connection_lifecycle = { active_binding_key: "binding-A" };
|
||
const reopenAuthority = retiredPhysicalReopenAuthority(
|
||
rendered,
|
||
rendered.devices[0].device_id,
|
||
"bridge",
|
||
);
|
||
const authorityA = connectionActionAuthoritySnapshot(rendered, "bridge");
|
||
assert.ok(authorityA);
|
||
assert.deepEqual(
|
||
physicalReopenClickAuthority(rendered, "bridge", authorityA, reopenAuthority),
|
||
authorityA,
|
||
);
|
||
|
||
const reconfigured = structuredClone(rendered);
|
||
reconfigured.connection_reconfiguration = connectionReconfiguration({
|
||
intent: "select-device",
|
||
revision: 1,
|
||
intentId: "reconfigure-B",
|
||
});
|
||
const authorityR1 = connectionActionAuthoritySnapshot(reconfigured, "bridge");
|
||
assert.equal(
|
||
physicalReopenClickAuthority(rendered, "bridge", authorityR1, reopenAuthority),
|
||
null,
|
||
"a rendered R0 CTA cannot borrow latest R1 before POST",
|
||
);
|
||
|
||
const rebound = structuredClone(rendered);
|
||
rebound.connection_lifecycle.active_binding_key = "binding-B";
|
||
const authorityB = connectionActionAuthoritySnapshot(rebound, "bridge");
|
||
assert.equal(
|
||
physicalReopenClickAuthority(rendered, "bridge", authorityB, reopenAuthority),
|
||
null,
|
||
"a rendered binding A CTA cannot borrow latest binding B before POST",
|
||
);
|
||
|
||
const requestA = {
|
||
reopening_id: "reopening-A",
|
||
expected_revision: reopenAuthority.expectedRevision,
|
||
expected_retirement_id: reopenAuthority.expectedRetirementId,
|
||
expected_transport_ref: reopenAuthority.expectedTransportRef,
|
||
expected_discovery_generation: reopenAuthority.expectedDiscoveryGeneration,
|
||
expected_desired_mode: reopenAuthority.expectedDesiredMode,
|
||
expected_desired_mode_revision: reopenAuthority.expectedDesiredModeRevision,
|
||
operator_confirmed: true,
|
||
reason: "device-returned-for-explicit-reconciliation",
|
||
};
|
||
const presentationA = {
|
||
key: "runtime-A:reopening-A:device",
|
||
snapshotRuntimeId: authorityA.snapshotRuntimeId,
|
||
request: requestA,
|
||
authority: authorityA,
|
||
};
|
||
assert.equal(
|
||
admitPhysicalReopenPresentation(null, presentationA, authorityA),
|
||
presentationA,
|
||
);
|
||
assert.equal(
|
||
admitPhysicalReopenPresentation(presentationA, presentationA, authorityA),
|
||
null,
|
||
"a double click is coalesced while A owns the action",
|
||
);
|
||
|
||
const authorityRuntimeB = {
|
||
...authorityA,
|
||
snapshotRuntimeId: "runtime-B",
|
||
};
|
||
const presentationB = {
|
||
...presentationA,
|
||
key: "runtime-B:reopening-B:device",
|
||
snapshotRuntimeId: "runtime-B",
|
||
request: { ...requestA, reopening_id: "reopening-B" },
|
||
authority: authorityRuntimeB,
|
||
};
|
||
assert.equal(physicalReopenPresentationIsCurrent(
|
||
presentationA,
|
||
authorityRuntimeB,
|
||
), false);
|
||
assert.equal(
|
||
admitPhysicalReopenPresentation(
|
||
presentationA,
|
||
presentationB,
|
||
authorityRuntimeB,
|
||
),
|
||
presentationB,
|
||
"runtime B synchronously replaces stale A without waiting for an effect",
|
||
);
|
||
assert.equal(physicalReopenSettlementIsCurrent(
|
||
presentationB,
|
||
presentationA.key,
|
||
authorityRuntimeB,
|
||
), false, "late A cannot message or clear B");
|
||
|
||
for (const [label, driftedAuthority] of [
|
||
["new reset revision", {
|
||
...authorityA,
|
||
desiredModeRevision: authorityA.desiredModeRevision + 1,
|
||
}],
|
||
["new discovery generation", {
|
||
...authorityA,
|
||
discoveryGeneration: authorityA.discoveryGeneration + 1,
|
||
}],
|
||
["new reconfiguration", authorityR1],
|
||
["new binding", authorityB],
|
||
]) {
|
||
assert.equal(physicalReopenPresentationIsCurrent(
|
||
presentationA,
|
||
driftedAuthority,
|
||
), false);
|
||
const replacement = {
|
||
...presentationA,
|
||
key: `runtime-A:reopening-B:${label}`,
|
||
request: {
|
||
...requestA,
|
||
reopening_id: `reopening-B-${label}`,
|
||
expected_desired_mode_revision: driftedAuthority.desiredModeRevision,
|
||
expected_discovery_generation: driftedAuthority.discoveryGeneration,
|
||
},
|
||
authority: driftedAuthority,
|
||
};
|
||
assert.equal(
|
||
admitPhysicalReopenPresentation(
|
||
presentationA,
|
||
replacement,
|
||
driftedAuthority,
|
||
),
|
||
replacement,
|
||
`${label} releases A synchronously so a current B click can start`,
|
||
);
|
||
}
|
||
});
|
||
|
||
test("same-runtime reconfiguration or binding drift after reopen suppresses Verify", async () => {
|
||
const request = {
|
||
reopening_id: "reopening-returned-k1",
|
||
expected_revision: 281,
|
||
expected_retirement_id: "retirement-old-stop",
|
||
expected_transport_ref: "F89438FA-55ED-85AD-EED7-734AC84746D8",
|
||
expected_discovery_generation: 13,
|
||
expected_desired_mode: "bridge",
|
||
expected_desired_mode_revision: 7,
|
||
operator_confirmed: true,
|
||
reason: "device-returned-for-explicit-reconciliation",
|
||
};
|
||
const committed = reopenedPhysicalState();
|
||
committed.connection_reconfiguration = connectionReconfiguration();
|
||
committed.connection_lifecycle = { active_binding_key: "binding-A" };
|
||
const expectedAuthority = connectionActionAuthoritySnapshot(committed, "bridge");
|
||
for (const drift of ["reconfiguration", "binding"]) {
|
||
const current = structuredClone(committed);
|
||
if (drift === "reconfiguration") {
|
||
current.connection_reconfiguration = connectionReconfiguration({
|
||
intent: "select-device",
|
||
revision: 1,
|
||
intentId: "reconfiguration-B",
|
||
});
|
||
} else {
|
||
current.connection_lifecycle.active_binding_key = "binding-B";
|
||
}
|
||
let verifyCalls = 0;
|
||
const result = await dispatchRetiredPhysicalReconciliationForCurrentRuntime({
|
||
snapshotRuntimeId: committed.snapshot_runtime_id,
|
||
request,
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
reopen: async () => ({ succeeded: true, observedState: committed }),
|
||
getCurrentState: () => current,
|
||
getCurrentTarget: () => recommendedConnectionRecoveryObservationTarget(current),
|
||
getCurrentAuthority: (mode) => connectionActionAuthoritySnapshot(current, mode),
|
||
expectedAuthority,
|
||
verify: async () => {
|
||
verifyCalls += 1;
|
||
return { succeeded: true };
|
||
},
|
||
});
|
||
assert.equal(result.verifyDispatched, false, drift);
|
||
assert.equal(verifyCalls, 0, drift);
|
||
}
|
||
});
|
||
|
||
test("a settled reset marker keeps the exact prior UUID as an ordinary Select row", () => {
|
||
const state = retiredPhysicalReopenReadyState();
|
||
state.connection_scenario_reset = {
|
||
reset_id: "reset-settled",
|
||
request_revision: 6,
|
||
revision: 7,
|
||
desired_mode: "bridge",
|
||
active: false,
|
||
settled_by_discovery_generation: 13,
|
||
local_session_closed: true,
|
||
previous_device_may_continue_scanning: false,
|
||
physical_disposition: "retired",
|
||
network_disposition: null,
|
||
device_command_performed: false,
|
||
network_write_performed: false,
|
||
automatic_scan: false,
|
||
operation_sequence: 21,
|
||
};
|
||
assert.deepEqual(scenarioResetPresentationBoundary(state, "bridge"), {
|
||
current: true,
|
||
active: false,
|
||
key: "runtime-reopened-k1:7",
|
||
});
|
||
const markup = renderProvisioningAfterSearch({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
});
|
||
assert.doesNotMatch(markup, /Прежний локальный сеанс закрыт/);
|
||
const select = buttonMarkupWithText(markup, "Выбрать");
|
||
assert.equal(select.length, 1);
|
||
assert.doesNotMatch(select[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(buttonMarkupWithText(markup, "Переподключиться").length, 0);
|
||
assert.doesNotMatch(markup, /Вернуть прежний K1 и проверить/);
|
||
});
|
||
|
||
test("an unresolved physical command has one explicit server-bound read-only recovery surface", () => {
|
||
const state = durableTopologyState();
|
||
state.snapshot_runtime_id = "runtime-physical-read-only";
|
||
const recoveryTransport = "F89438FA-55ED-85AD-EED7-734AC84746D8";
|
||
state.physical_command = {
|
||
status: "unresolved",
|
||
reason_code: "physical-command-reconciliation-required",
|
||
requires_reconciliation: true,
|
||
resolved_active_recovery_required: false,
|
||
automatic_replay_allowed: false,
|
||
normal_session_recovery_supported: false,
|
||
runtime_bound: true,
|
||
reconciliation_ready: true,
|
||
observed_session_state: "scan_stopping",
|
||
active_operation_id: "old-stop-operation",
|
||
record: {
|
||
revision: 306,
|
||
operation_id: "old-stop-operation",
|
||
action: "stop",
|
||
stage: "observing",
|
||
resolution: null,
|
||
connection: {
|
||
transport_ref: recoveryTransport,
|
||
connection_mode: "bridge",
|
||
target_ipv4: "192.168.68.51",
|
||
},
|
||
},
|
||
};
|
||
state.connection_attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: "historical-network-attempt-after-powered-off-start",
|
||
connection_mode: "bridge",
|
||
status: "failed",
|
||
phase: "network_applied",
|
||
control_state: "control_not_ready",
|
||
stage: "host-route-and-control-endpoint",
|
||
public_error_code: "control-bootstrap-failed",
|
||
side_effect_status: "network-applied",
|
||
safe_next_action: "manual-recovery-required",
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: OBSERVED_AT,
|
||
timeline: [],
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false, retired_transport_refs: [] },
|
||
allowed_actions: ["scan-ble", "observe-configured-device-network"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
"provision-fresh-device": {
|
||
allowed: false,
|
||
reason_codes: ["physical-command-reconciliation-required"],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: null,
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
"observe-configured-device-network": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: recoveryTransport,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
state.devices = [
|
||
{
|
||
device_id: recoveryTransport.toLowerCase(),
|
||
name: "XGR-A46BE7",
|
||
rssi: -42,
|
||
connectable: true,
|
||
likely_k1: true,
|
||
},
|
||
{
|
||
device_id: "nearby-unrelated-device",
|
||
name: "Nearby unrelated device",
|
||
rssi: -48,
|
||
connectable: true,
|
||
likely_k1: false,
|
||
},
|
||
];
|
||
|
||
const markup = renderToStaticMarkup(createElement(K1ProvisioningPipeline, {
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
}));
|
||
const phaseDetail = physicalRecoveryConnectionDetail(state);
|
||
assert.match(phaseDetail, /Проверьте состояние прежнего K1/);
|
||
assert.match(phaseDetail, /сохранённое системой подключение/);
|
||
assert.match(phaseDetail, /не отправляет START, STOP или настройки сети/);
|
||
assert.doesNotMatch(phaseDetail, /read-only/);
|
||
assert.doesNotMatch(phaseDetail, /серверн.*цел/i);
|
||
assert.doesNotMatch(markup, /read-only|серверн.*цел/i);
|
||
assert.doesNotMatch(phaseDetail, /[Пп]одтвердите питание|локально исключить/);
|
||
const reconnect = buttonMarkupWithText(markup, "Переподключиться");
|
||
const connectNew = buttonMarkupWithText(markup, "Подключить новый K1");
|
||
assert.equal(reconnect.length, 1);
|
||
assert.equal(connectNew.length, 1);
|
||
assert.doesNotMatch(reconnect[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.doesNotMatch(connectNew[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.doesNotMatch(
|
||
markup,
|
||
/Предыдущая команда завершилась|Проверить прежний K1 без изменений|Выберите способ восстановления|Подтверждаю:|Питание|индикатор горит/,
|
||
);
|
||
assert.doesNotMatch(markup, /<span>02<\/span>|<span>03<\/span>/);
|
||
assert.doesNotMatch(markup, /Попытка настройки завершена|Проверить подключение без изменения сети/);
|
||
assert.doesNotMatch(markup, /nearby-unrelated-device/);
|
||
assert.doesNotMatch(markup, />Применить<|>Выбрать<|Обновить Bluetooth-поиск/);
|
||
assert.doesNotMatch(markup, /old-stop-operation/);
|
||
|
||
const searchedMarkup = renderProvisioningAfterSearch({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
});
|
||
assert.equal(buttonMarkupWithText(searchedMarkup, "Переподключиться").length, 1);
|
||
assert.equal(buttonMarkupWithText(searchedMarkup, "Подключить новый K1").length, 1);
|
||
assert.doesNotMatch(
|
||
searchedMarkup,
|
||
/Результаты последнего Bluetooth-поиска|nearby-unrelated-device|>Выбрать<|>Применить</,
|
||
);
|
||
|
||
const quickRecoveryInsideBridgeState = structuredClone(state);
|
||
quickRecoveryInsideBridgeState.physical_command.record.connection.connection_mode =
|
||
"quick-connect";
|
||
quickRecoveryInsideBridgeState.physical_command.record.connection.target_ipv4 =
|
||
"192.168.56.1";
|
||
quickRecoveryInsideBridgeState.semantic_topology_store.record.connection_mode =
|
||
"quick-connect";
|
||
quickRecoveryInsideBridgeState.semantic_topology_store.record.ipv4 = "192.168.56.1";
|
||
quickRecoveryInsideBridgeState.connection_policy.actions[
|
||
"observe-configured-device-network"
|
||
].required_connection_mode = "quick-connect";
|
||
const quickRecoveryInsideBridgeMarkup = renderToStaticMarkup(createElement(
|
||
K1ProvisioningPipeline,
|
||
{
|
||
controller: provisioningController(quickRecoveryInsideBridgeState),
|
||
desiredMode: "bridge",
|
||
},
|
||
));
|
||
assert.equal(
|
||
buttonMarkupWithText(
|
||
quickRecoveryInsideBridgeMarkup,
|
||
"Переподключиться",
|
||
).length,
|
||
0,
|
||
);
|
||
assert.equal(
|
||
buttonMarkupWithText(
|
||
quickRecoveryInsideBridgeMarkup,
|
||
"Подключить новый K1",
|
||
).length,
|
||
1,
|
||
);
|
||
|
||
const selectedQuickRecoveryState = structuredClone(
|
||
quickRecoveryInsideBridgeState,
|
||
);
|
||
selectedQuickRecoveryState.desired_connection_mode = "quick-connect";
|
||
const selectedQuickRecoveryMarkup = renderToStaticMarkup(createElement(
|
||
K1ProvisioningPipeline,
|
||
{
|
||
controller: provisioningController(selectedQuickRecoveryState),
|
||
desiredMode: "quick-connect",
|
||
},
|
||
));
|
||
assert.equal(
|
||
buttonMarkupWithText(selectedQuickRecoveryMarkup, "Переподключиться").length,
|
||
1,
|
||
);
|
||
|
||
const networkSetupState = structuredClone(state);
|
||
networkSetupState.connection_verification = {
|
||
status: "unreachable",
|
||
lease_state: "disconnected",
|
||
network_reachability: "unreachable",
|
||
reason_code: "connection-verify-address-unavailable",
|
||
};
|
||
networkSetupState.last_operation = {
|
||
action: "connection.verify",
|
||
status: "failed",
|
||
error: { code: "connection-verify-device-not-rediscovered" },
|
||
};
|
||
const networkSetupMarkup = renderToStaticMarkup(createElement(
|
||
K1ProvisioningPipeline,
|
||
{
|
||
controller: provisioningController(networkSetupState),
|
||
desiredMode: "bridge",
|
||
},
|
||
));
|
||
const networkSetupReconnect = buttonMarkupWithText(
|
||
networkSetupMarkup,
|
||
"Переподключиться",
|
||
);
|
||
assert.equal(networkSetupReconnect.length, 1);
|
||
assert.doesNotMatch(networkSetupReconnect[0], /\bdisabled(?:=|\s|>)/);
|
||
const networkSetupAction = buttonMarkupWithText(
|
||
networkSetupMarkup,
|
||
"Подключить K1 к общей сети",
|
||
);
|
||
assert.equal(networkSetupAction.length, 1);
|
||
assert.doesNotMatch(networkSetupAction[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.match(
|
||
networkSetupMarkup,
|
||
/K1 не подключён к сохранённой общей сети/,
|
||
);
|
||
assert.match(networkSetupMarkup, /не сообщил адрес Bridge/);
|
||
assert.doesNotMatch(
|
||
networkSetupMarkup,
|
||
/Локальная операция завершилась ошибкой|Проверьте питание и сеть/,
|
||
);
|
||
assert.match(
|
||
physicalRecoveryConnectionDetail(networkSetupState),
|
||
/K1 ответил по Bluetooth, но не подключён к сохранённой общей сети/,
|
||
);
|
||
|
||
const policyDeniedState = structuredClone(state);
|
||
policyDeniedState.connection_policy.allowed_actions = ["scan-ble"];
|
||
delete policyDeniedState.connection_policy.actions["observe-configured-device-network"];
|
||
const policyDeniedMarkup = renderProvisioningAfterSearch({
|
||
controller: provisioningController(policyDeniedState),
|
||
desiredMode: "bridge",
|
||
});
|
||
assert.equal(buttonMarkupWithText(policyDeniedMarkup, "Переподключиться").length, 0);
|
||
const deniedNew = buttonMarkupWithText(policyDeniedMarkup, "Подключить новый K1");
|
||
assert.equal(deniedNew.length, 1);
|
||
assert.doesNotMatch(deniedNew[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.doesNotMatch(
|
||
policyDeniedMarkup,
|
||
/nearby-unrelated-device|Старый K1 недоступен|Подтверждаю:|>Применить</,
|
||
);
|
||
|
||
const pendingMarkup = renderProvisioningWithCurrentPendingAction({
|
||
controller: {
|
||
...provisioningController(state),
|
||
pendingAction: "verify",
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
isConnectionActionAuthorityCurrent: () => true,
|
||
},
|
||
desiredMode: "bridge",
|
||
}, { publicSearch: false });
|
||
const pendingReconnect = buttonMarkupWithText(
|
||
pendingMarkup,
|
||
"Переподключение…",
|
||
);
|
||
assert.equal(pendingReconnect.length, 1);
|
||
assert.match(pendingReconnect[0], /disabled/);
|
||
assert.match(pendingReconnect[0], /aria-busy="true"/);
|
||
assert.match(pendingReconnect[0], /nodedc-activity-indicator/);
|
||
const pendingConnectNew = buttonMarkupWithText(
|
||
pendingMarkup,
|
||
"Подключить новый K1",
|
||
);
|
||
assert.equal(pendingConnectNew.length, 1);
|
||
assert.match(pendingConnectNew[0], /disabled/);
|
||
assert.doesNotMatch(pendingMarkup, /connection-action-progress|Подтверждаю:/);
|
||
|
||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||
const physicalRecovery = sourceSlice(
|
||
source,
|
||
"const verifyPhysicalRecovery",
|
||
"const prepareReconfiguration",
|
||
);
|
||
assert.equal((physicalRecovery.match(/await verifyConnection\(/g) ?? []).length, 1);
|
||
assert.doesNotMatch(
|
||
physicalRecovery,
|
||
/scanWithResult\(|connect\(|retireUnavailable|reopenRetired|reconcilePhysicalCommand/,
|
||
);
|
||
assert.match(physicalRecovery, /physicalRecoveryTarget\?\.serverBound/);
|
||
assert.match(physicalRecovery, /surfaceErrors: false/);
|
||
assert.match(
|
||
physicalRecovery,
|
||
/physicalRecoveryTarget\.connectionMode !== connectionMode/,
|
||
);
|
||
assert.doesNotMatch(
|
||
physicalRecovery,
|
||
/commitDesiredModeForExplicitAction|selectConnectionMode\(/,
|
||
);
|
||
});
|
||
|
||
test("supervisor DeviceInfo Verify outranks a stale exact fresh row for physical recovery", () => {
|
||
const state = durableTopologyState();
|
||
const recoveryTransport = "F89438FA-55ED-85AD-EED7-734AC84746D8";
|
||
state.snapshot_runtime_id = "runtime-terminal-control-physical-recovery";
|
||
state.semantic_topology_store.record.transport_ref = recoveryTransport;
|
||
state.physical_command = {
|
||
status: "unresolved",
|
||
reason_code: "physical-command-reconciliation-required",
|
||
requires_reconciliation: true,
|
||
resolved_active_recovery_required: false,
|
||
reconciliation_ready: true,
|
||
record: {
|
||
revision: 583,
|
||
operation_id: "old-start-operation",
|
||
action: "start",
|
||
stage: "observing",
|
||
resolution: null,
|
||
connection: {
|
||
transport_ref: recoveryTransport,
|
||
connection_mode: "bridge",
|
||
target_ipv4: "192.168.68.52",
|
||
},
|
||
},
|
||
};
|
||
const deniedObservation = (targetSource) => ({
|
||
allowed: false,
|
||
reason_codes: ["control-session-not-admissible-for-network-change"],
|
||
target_source: targetSource,
|
||
required_transport_ref: recoveryTransport,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
});
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false, retired_transport_refs: [] },
|
||
recommended_action: "scan-ble",
|
||
allowed_actions: [
|
||
"scan-ble",
|
||
"observe-fresh-device-network",
|
||
"inspect-configured-endpoint",
|
||
"verify-control-device-info",
|
||
],
|
||
actions: {
|
||
"observe-fresh-device-network": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: recoveryTransport,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
"observe-current-device-network": deniedObservation(
|
||
"retained-current-process",
|
||
),
|
||
"observe-configured-device-network": deniedObservation(
|
||
"durable-configured-state",
|
||
),
|
||
"verify-control-device-info": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "connection-supervisor",
|
||
required_transport_ref: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
state.devices = [{
|
||
device_id: recoveryTransport,
|
||
name: "Projected K1",
|
||
connectable: true,
|
||
likely_k1: true,
|
||
}];
|
||
state.ble_discovery_generation = 2;
|
||
|
||
assert.deepEqual(readOnlyConnectionObservationTarget(state), {
|
||
action: "verify-control-device-info",
|
||
deviceId: recoveryTransport,
|
||
connectionMode: "bridge",
|
||
source: "durable-configured-state",
|
||
serverBound: true,
|
||
expectedDiscoveryGeneration: null,
|
||
});
|
||
|
||
const markup = renderToStaticMarkup(createElement(K1ProvisioningPipeline, {
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
}));
|
||
const reconnect = buttonMarkupWithText(markup, "Переподключиться");
|
||
assert.equal(reconnect.length, 1);
|
||
assert.doesNotMatch(reconnect[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(buttonMarkupWithText(markup, "Подключить новый K1").length, 1);
|
||
|
||
const mismatched = structuredClone(state);
|
||
mismatched.semantic_topology_store.record.transport_ref = "another-k1";
|
||
mismatched.connection_policy.actions["observe-fresh-device-network"].allowed = false;
|
||
assert.equal(readOnlyConnectionObservationTarget(mismatched), null);
|
||
});
|
||
|
||
test("unavailable physical replacement stays CAS-protected behind the simple new-device path", () => {
|
||
const state = durableTopologyState();
|
||
const transportRef = "F89438FA-55ED-85AD-EED7-734AC84746D8";
|
||
state.snapshot_runtime_id = "runtime-retire-old-k1";
|
||
state.physical_command = {
|
||
status: "unresolved",
|
||
reason_code: "physical-command-reconciliation-required",
|
||
requires_reconciliation: true,
|
||
resolved_active_recovery_required: false,
|
||
automatic_replay_allowed: false,
|
||
normal_session_recovery_supported: false,
|
||
runtime_bound: false,
|
||
reconciliation_ready: true,
|
||
observed_session_state: "unknown",
|
||
active_operation_id: "old-start-operation",
|
||
operator_retirement: {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
expected_operation_id: "old-start-operation",
|
||
expected_revision: 41,
|
||
expected_transport_ref: transportRef,
|
||
physical_outcome: "unknown",
|
||
device_io_performed: false,
|
||
automatic_retry: false,
|
||
},
|
||
record: {
|
||
revision: 41,
|
||
operation_id: "old-start-operation",
|
||
action: "start",
|
||
stage: "observing",
|
||
resolution: null,
|
||
connection: {
|
||
transport_ref: transportRef,
|
||
connection_mode: "bridge",
|
||
},
|
||
},
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false, retired_transport_refs: [] },
|
||
allowed_actions: ["retire-unavailable-physical-target"],
|
||
actions: {
|
||
"provision-fresh-device": {
|
||
allowed: false,
|
||
reason_codes: ["physical-command-reconciliation-required"],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: null,
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
"retire-unavailable-physical-target": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-physical-command",
|
||
required_transport_ref: transportRef,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: false,
|
||
physical_command_allowed: false,
|
||
physical_outcome: "unknown",
|
||
device_write_performed: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
assert.deepEqual(unavailablePhysicalRetirementAuthority(state), {
|
||
expectedOperationId: "old-start-operation",
|
||
expectedRevision: 41,
|
||
expectedTransportRef: transportRef,
|
||
});
|
||
const markup = renderToStaticMarkup(createElement(K1ProvisioningPipeline, {
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
}));
|
||
const connectNew = buttonMarkupWithText(markup, "Подключить новый K1");
|
||
assert.equal(connectNew.length, 1);
|
||
assert.doesNotMatch(connectNew[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(buttonMarkupWithText(markup, "Переподключиться").length, 0);
|
||
assert.doesNotMatch(markup, /<span>02<\/span>|<span>03<\/span>/);
|
||
assert.doesNotMatch(
|
||
markup,
|
||
/Старый K1 недоступен|Подтверждаю:|Выберите способ восстановления|Проверить прежний K1 без изменений|[Пп]итани/,
|
||
);
|
||
const phaseDetail = physicalRecoveryConnectionDetail(state);
|
||
assert.match(phaseDetail, /можно локально исключить без связи с устройством/);
|
||
assert.match(phaseDetail, /не отправляет START, STOP или настройки сети/);
|
||
assert.doesNotMatch(phaseDetail, /[Пп]итани|read-only/);
|
||
|
||
const denied = structuredClone(state);
|
||
denied.connection_policy.allowed_actions = [];
|
||
denied.connection_policy.actions["retire-unavailable-physical-target"].allowed = false;
|
||
denied.connection_policy.actions["retire-unavailable-physical-target"].reason_codes = [
|
||
"physical-command-retirement-state-unsafe",
|
||
];
|
||
assert.equal(unavailablePhysicalRetirementAuthority(denied), null);
|
||
const deniedMarkup = renderProvisioning({
|
||
controller: provisioningController(denied),
|
||
desiredMode: "bridge",
|
||
});
|
||
const deniedNew = buttonMarkupWithText(deniedMarkup, "Подключить новый K1");
|
||
assert.equal(deniedNew.length, 1);
|
||
assert.doesNotMatch(deniedNew[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(buttonMarkupWithText(deniedMarkup, "Переподключиться").length, 0);
|
||
assert.doesNotMatch(deniedMarkup, /Старый K1 недоступен|Подтверждаю:/);
|
||
assert.doesNotMatch(
|
||
physicalRecoveryConnectionDetail(denied),
|
||
/можно локально исключить/,
|
||
);
|
||
|
||
const resolvedActive = structuredClone(state);
|
||
resolvedActive.physical_command.status = "resolved";
|
||
resolvedActive.physical_command.requires_reconciliation = false;
|
||
resolvedActive.physical_command.resolved_active_recovery_required = true;
|
||
resolvedActive.physical_command.observed_session_state = "scanning";
|
||
resolvedActive.physical_command.record.stage = "resolved";
|
||
resolvedActive.physical_command.record.resolution = "start-active-observed";
|
||
resolvedActive.connection_policy.actions["provision-fresh-device"].reason_codes = [
|
||
"physical-device-already-active",
|
||
];
|
||
const resolvedActiveMarkup = renderToStaticMarkup(createElement(
|
||
K1ProvisioningPipeline,
|
||
{
|
||
controller: provisioningController(resolvedActive),
|
||
desiredMode: "bridge",
|
||
},
|
||
));
|
||
const resolvedNew = buttonMarkupWithText(
|
||
resolvedActiveMarkup,
|
||
"Подключить новый K1",
|
||
);
|
||
assert.equal(resolvedNew.length, 1);
|
||
assert.doesNotMatch(resolvedNew[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(
|
||
buttonMarkupWithText(resolvedActiveMarkup, "Переподключиться").length,
|
||
0,
|
||
);
|
||
assert.doesNotMatch(resolvedActiveMarkup, /[Пп]итани/);
|
||
assert.doesNotMatch(resolvedActiveMarkup, /<span>02<\/span>|<span>03<\/span>/);
|
||
|
||
const bothAllowed = structuredClone(state);
|
||
bothAllowed.semantic_topology_store.record.transport_ref = transportRef;
|
||
bothAllowed.connection_policy.allowed_actions = [
|
||
"observe-configured-device-network",
|
||
"retire-unavailable-physical-target",
|
||
];
|
||
bothAllowed.connection_policy.actions["observe-configured-device-network"] = {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: transportRef,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
};
|
||
const bothMarkup = renderToStaticMarkup(createElement(
|
||
K1ProvisioningPipeline,
|
||
{
|
||
controller: provisioningController(bothAllowed),
|
||
desiredMode: "bridge",
|
||
},
|
||
));
|
||
const bothReconnect = buttonMarkupWithText(bothMarkup, "Переподключиться");
|
||
const bothNew = buttonMarkupWithText(bothMarkup, "Подключить новый K1");
|
||
assert.equal(bothReconnect.length, 1);
|
||
assert.equal(bothNew.length, 1);
|
||
assert.doesNotMatch(bothReconnect[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.doesNotMatch(bothNew[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.ok(
|
||
bothMarkup.indexOf("Переподключиться")
|
||
< bothMarkup.indexOf("Подключить новый K1"),
|
||
"read-only reconnect must be first",
|
||
);
|
||
assert.doesNotMatch(
|
||
bothMarkup,
|
||
/Выберите способ восстановления|Если прежний K1 снова доступен|Если прежний K1 недоступен постоянно|Подтверждаю:/,
|
||
);
|
||
assert.doesNotMatch(bothMarkup, /[Пп]итани|индикатор горит/);
|
||
assert.doesNotMatch(bothMarkup, /<span>02<\/span>|<span>03<\/span>/);
|
||
const bothPhaseDetail = physicalRecoveryConnectionDetail(bothAllowed);
|
||
assert.match(bothPhaseDetail, /Если прежний K1 снова доступен/);
|
||
assert.match(bothPhaseDetail, /недоступен постоянно или заменён/);
|
||
assert.match(bothPhaseDetail, /не отправляет START, STOP или настройки сети/);
|
||
assert.match(bothPhaseDetail, /без связи с устройством/);
|
||
assert.doesNotMatch(bothPhaseDetail, /read-only/);
|
||
assert.doesNotMatch(bothMarkup, /read-only/);
|
||
|
||
const bothPendingMarkup = renderProvisioningWithCurrentPendingAction({
|
||
controller: {
|
||
...provisioningController(bothAllowed),
|
||
pendingAction: "verify",
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
isConnectionActionAuthorityCurrent: () => true,
|
||
},
|
||
desiredMode: "bridge",
|
||
}, { publicSearch: false });
|
||
const bothPendingReconnect = buttonMarkupWithText(
|
||
bothPendingMarkup,
|
||
"Переподключение…",
|
||
);
|
||
assert.equal(bothPendingReconnect.length, 1);
|
||
assert.match(bothPendingReconnect[0], /disabled/);
|
||
assert.match(bothPendingReconnect[0], /aria-busy="true"/);
|
||
assert.match(bothPendingReconnect[0], /nodedc-activity-indicator/);
|
||
assert.match(bothPendingMarkup, /Прежнее подключение не подтверждено/);
|
||
assert.doesNotMatch(bothPendingMarkup, /connection-action-progress|Подтверждаю:/);
|
||
|
||
const bothResolvedActive = structuredClone(bothAllowed);
|
||
bothResolvedActive.physical_command.status = "resolved";
|
||
bothResolvedActive.physical_command.requires_reconciliation = false;
|
||
bothResolvedActive.physical_command.resolved_active_recovery_required = true;
|
||
bothResolvedActive.physical_command.observed_session_state = "scanning";
|
||
bothResolvedActive.physical_command.record.stage = "resolved";
|
||
bothResolvedActive.physical_command.record.resolution = "start-active-observed";
|
||
bothResolvedActive.connection_policy.actions[
|
||
"provision-fresh-device"
|
||
].reason_codes = ["physical-device-already-active"];
|
||
const bothResolvedActiveMarkup = renderToStaticMarkup(createElement(
|
||
K1ProvisioningPipeline,
|
||
{
|
||
controller: provisioningController(bothResolvedActive),
|
||
desiredMode: "bridge",
|
||
},
|
||
));
|
||
assert.equal(
|
||
buttonMarkupWithText(bothResolvedActiveMarkup, "Переподключиться").length,
|
||
1,
|
||
);
|
||
assert.equal(
|
||
buttonMarkupWithText(bothResolvedActiveMarkup, "Подключить новый K1").length,
|
||
1,
|
||
);
|
||
assert.doesNotMatch(
|
||
bothResolvedActiveMarkup,
|
||
/<span>02<\/span>|<span>03<\/span>/,
|
||
);
|
||
});
|
||
|
||
test("physical recovery new-device action performs only an explicit scenario reset", async () => {
|
||
const state = reopenedPhysicalState();
|
||
const resetRequests = [];
|
||
let desiredModeChanges = 0;
|
||
let scanCalls = 0;
|
||
let connectCalls = 0;
|
||
let verifyCalls = 0;
|
||
let reopenCalls = 0;
|
||
let markResetDispatched;
|
||
const resetDispatched = new Promise((resolve) => {
|
||
markResetDispatched = resolve;
|
||
});
|
||
const controller = {
|
||
...provisioningController(state),
|
||
selectConnectionMode: async (request) => {
|
||
resetRequests.push(request);
|
||
markResetDispatched();
|
||
return true;
|
||
},
|
||
scanWithResult: async () => {
|
||
scanCalls += 1;
|
||
return { succeeded: false };
|
||
},
|
||
connect: async () => {
|
||
connectCalls += 1;
|
||
return { succeeded: false };
|
||
},
|
||
verifyConnection: async () => {
|
||
verifyCalls += 1;
|
||
return { succeeded: false };
|
||
},
|
||
reopenRetiredPhysicalReconciliation: async () => {
|
||
reopenCalls += 1;
|
||
return { succeeded: false };
|
||
},
|
||
};
|
||
const props = {
|
||
controller,
|
||
desiredMode: "bridge",
|
||
onDesiredModeChange: async (mode) => {
|
||
assert.equal(mode, "bridge");
|
||
desiredModeChanges += 1;
|
||
},
|
||
};
|
||
const markup = renderProvisioning(props);
|
||
const resetButtons = buttonMarkupWithText(markup, "Подключить новый K1");
|
||
assert.equal(resetButtons.length, 1);
|
||
assert.doesNotMatch(resetButtons[0], /\bdisabled(?:=|\s|>)/);
|
||
|
||
const tree = captureProvisioningTree(props);
|
||
const connectNew = actionByLabel(tree, "Подключить новый K1");
|
||
assert.ok(connectNew);
|
||
connectNew.props.onClick();
|
||
await Promise.race([
|
||
resetDispatched,
|
||
new Promise((_, reject) => setTimeout(
|
||
() => reject(new Error("new-device action did not dispatch scenario reset")),
|
||
100,
|
||
)),
|
||
]);
|
||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||
|
||
assert.equal(resetRequests.length, 1);
|
||
assert.equal(resetRequests[0].connection_mode, "bridge");
|
||
assert.equal(
|
||
resetRequests[0].expected_revision,
|
||
state.desired_connection_mode_revision,
|
||
);
|
||
assert.equal(resetRequests[0].reset_scenario, true);
|
||
assert.equal(typeof resetRequests[0].reset_id, "string");
|
||
assert.ok(resetRequests[0].reset_id.length > 0);
|
||
assert.equal(desiredModeChanges, 1);
|
||
assert.equal(scanCalls, 0);
|
||
assert.equal(connectCalls, 0);
|
||
assert.equal(verifyCalls, 0);
|
||
assert.equal(reopenCalls, 0);
|
||
});
|
||
|
||
test("topology switch stays silent for two seconds and then uses only the inline loader", async () => {
|
||
const state = terminalConnectionRecoveryState();
|
||
state.connection_attempt = null;
|
||
let resolveReset;
|
||
const resetSettlement = new Promise((resolve) => {
|
||
resolveReset = resolve;
|
||
});
|
||
const resetRequests = [];
|
||
const controller = {
|
||
...provisioningController(state),
|
||
selectConnectionMode: (request) => {
|
||
resetRequests.push(request);
|
||
return resetSettlement;
|
||
},
|
||
};
|
||
const props = {
|
||
controller,
|
||
desiredMode: "bridge",
|
||
onDesiredModeChange: () => undefined,
|
||
};
|
||
const originalSetTimeout = globalThis.setTimeout;
|
||
const originalClearTimeout = globalThis.clearTimeout;
|
||
const timers = [];
|
||
globalThis.setTimeout = (callback, delay, ...args) => {
|
||
const timer = { callback, delay, args, cleared: false };
|
||
timers.push(timer);
|
||
return timer;
|
||
};
|
||
globalThis.clearTimeout = (timer) => {
|
||
timer.cleared = true;
|
||
};
|
||
const harness = createStatefulProvisioningHarness(props);
|
||
try {
|
||
let tree = harness.render();
|
||
harness.flushEffects();
|
||
const modeSelect = elementByProp(tree, "label", "Способ подключения");
|
||
assert.ok(modeSelect);
|
||
modeSelect.props.onChange("quick-connect");
|
||
assert.equal(resetRequests.length, 1);
|
||
|
||
tree = harness.render();
|
||
harness.flushEffects();
|
||
let markup = renderToStaticMarkup(tree);
|
||
assert.doesNotMatch(markup, /Переключаем способ подключения/);
|
||
assert.doesNotMatch(markup, /Завершение прежней локальной границы/);
|
||
const progressTimer = timers.find(
|
||
(timer) => timer.delay === LOCAL_OPERATION_PROGRESS_DELAY_MILLISECONDS,
|
||
);
|
||
assert.ok(progressTimer);
|
||
assert.equal(progressTimer.cleared, false);
|
||
|
||
progressTimer.callback(...progressTimer.args);
|
||
tree = harness.render();
|
||
markup = renderToStaticMarkup(tree);
|
||
assert.match(markup, /Переключаем способ подключения/);
|
||
assert.match(markup, /connection-action-progress/);
|
||
assert.doesNotMatch(markup, /dialog|modal|window/i);
|
||
|
||
resolveReset(true);
|
||
await Promise.resolve();
|
||
await Promise.resolve();
|
||
tree = harness.render();
|
||
harness.flushEffects();
|
||
markup = renderToStaticMarkup(tree);
|
||
assert.doesNotMatch(markup, /Переключаем способ подключения/);
|
||
} finally {
|
||
harness.dispose();
|
||
globalThis.setTimeout = originalSetTimeout;
|
||
globalThis.clearTimeout = originalClearTimeout;
|
||
}
|
||
});
|
||
|
||
test("physical retirement dispatch refuses a rendered runtime A after runtime B is current", async () => {
|
||
let currentRuntimeId = "runtime-B";
|
||
let mutationRequests = 0;
|
||
const stale = await dispatchUnavailablePhysicalRetirementForCurrentRuntime(
|
||
"runtime-A",
|
||
(expectedSnapshotRuntimeId) => expectedSnapshotRuntimeId === currentRuntimeId,
|
||
async () => {
|
||
mutationRequests += 1;
|
||
return { succeeded: true };
|
||
},
|
||
);
|
||
assert.deepEqual(stale, { dispatched: false, result: null });
|
||
assert.equal(mutationRequests, 0);
|
||
|
||
currentRuntimeId = "runtime-A";
|
||
const current = await dispatchUnavailablePhysicalRetirementForCurrentRuntime(
|
||
"runtime-A",
|
||
(expectedSnapshotRuntimeId) => expectedSnapshotRuntimeId === currentRuntimeId,
|
||
async (expectedSnapshotRuntimeId) => {
|
||
mutationRequests += 1;
|
||
return { expectedSnapshotRuntimeId };
|
||
},
|
||
);
|
||
assert.deepEqual(current, {
|
||
dispatched: true,
|
||
result: { expectedSnapshotRuntimeId: "runtime-A" },
|
||
});
|
||
assert.equal(mutationRequests, 1);
|
||
|
||
const runtimeSource = readFileSync(new URL(
|
||
"../../../plugins/xgrids-k1/frontend/src/useXgridsK1Runtime.ts",
|
||
import.meta.url,
|
||
), "utf8");
|
||
const retirementHook = sourceSlice(
|
||
runtimeSource,
|
||
"const retireUnavailablePhysicalCommandWithResult",
|
||
"const retireUnavailablePhysicalCommand = useCallback",
|
||
);
|
||
assert.match(
|
||
retirementHook,
|
||
/!isSnapshotRuntimeCurrent\(exactSnapshotRuntimeId\)[\s\S]*?expected_snapshot_runtime_id:\s*exactSnapshotRuntimeId/,
|
||
);
|
||
assert.doesNotMatch(
|
||
retirementHook,
|
||
/expected_snapshot_runtime_id:\s*expectedSnapshotRuntimeId\(\)/,
|
||
);
|
||
});
|
||
|
||
|
||
|
||
test("an explicit network action keeps one disabled Step-2 form with an in-place loader", () => {
|
||
const state = durableTopologyState();
|
||
state.snapshot_runtime_id = "runtime-network-action";
|
||
state.desired_connection_mode_revision = 7;
|
||
state.ble_discovery_generation = 13;
|
||
state.semantic_topology_store = {
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: null,
|
||
};
|
||
const device = {
|
||
device_id: "f89438fa-55ed-85ad-eed7-734ac84746d8",
|
||
name: "XGR-A46BE7",
|
||
rssi: -51,
|
||
connectable: true,
|
||
likely_k1: true,
|
||
};
|
||
state.devices = [device];
|
||
state.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,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
const presentation = provisioningAttemptPresentation({
|
||
snapshotRuntimeId: state.snapshot_runtime_id,
|
||
deviceId: device.device_id,
|
||
});
|
||
const markup = renderProvisioningWithAttempt({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
}, presentation);
|
||
|
||
assert.doesNotMatch(markup, /class="connection-action-progress"/);
|
||
assert.match(markup, /aria-busy="true"/);
|
||
assert.equal((markup.match(/class="nodedc-activity-indicator"/g) ?? []).length, 1);
|
||
assert.match(markup, /Устройство выбрано/);
|
||
assert.match(markup, /XGR-A46BE7/);
|
||
assert.match(markup, /<span>02<\/span>/);
|
||
assert.match(markup, /Применение настроек…/);
|
||
assert.match(markup, /FIELD-NET/);
|
||
assert.match(markup, /Пароль передан/);
|
||
assert.doesNotMatch(markup, /secret/);
|
||
assert.equal((markup.match(/<input[^>]*disabled=""/g) ?? []).length, 2);
|
||
assert.doesNotMatch(markup, />Применить<\/button>/);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
test("Apply presentation stays correlated through ACK, bootstrap, terminal and connected projections", () => {
|
||
const presentation = provisioningAttemptPresentation({
|
||
deviceId: "ble-k1-001",
|
||
});
|
||
const initialState = {
|
||
...durableTopologyState(),
|
||
snapshot_runtime_id: presentation.snapshotRuntimeId,
|
||
};
|
||
assert.equal(
|
||
provisioningAttemptViewState(presentation, initialState),
|
||
"submitting",
|
||
);
|
||
|
||
const operation = {
|
||
operation_id: "network-operation-one",
|
||
action: "network.provision",
|
||
status: "succeeded",
|
||
idempotency_key: presentation.idempotencyKey,
|
||
};
|
||
assert.equal(
|
||
provisioningAttemptViewState({
|
||
...presentation,
|
||
localPhase: "failed",
|
||
}, {
|
||
...initialState,
|
||
operations: [{ ...operation, status: "running" }],
|
||
}),
|
||
"settling",
|
||
"an authoritative running operation supersedes a presentation error",
|
||
);
|
||
assert.equal(
|
||
provisioningAttemptViewState(presentation, {
|
||
...initialState,
|
||
operations: [operation],
|
||
}),
|
||
"settling",
|
||
"a fast network ACK is not yet a connected result",
|
||
);
|
||
|
||
const correlated = {
|
||
...presentation,
|
||
attemptId: operation.operation_id,
|
||
localPhase: "settling",
|
||
};
|
||
const attempt = {
|
||
attempt_id: operation.operation_id,
|
||
connection_mode: "bridge",
|
||
status: "running",
|
||
phase: "network_applied",
|
||
control_state: "unknown",
|
||
safe_next_action: "wait-for-current-attempt",
|
||
};
|
||
assert.equal(
|
||
provisioningAttemptViewState(correlated, {
|
||
...initialState,
|
||
operations: [operation],
|
||
connection_attempt: attempt,
|
||
}),
|
||
"settling",
|
||
);
|
||
assert.equal(
|
||
provisioningAttemptViewState(correlated, {
|
||
...initialState,
|
||
operations: [operation],
|
||
connection_attempt: {
|
||
...attempt,
|
||
status: "succeeded",
|
||
control_state: "ready",
|
||
safe_next_action: "start-acquisition",
|
||
},
|
||
}),
|
||
"settling",
|
||
"ready child success waits for the exact reachable lease projection",
|
||
);
|
||
assert.equal(
|
||
provisioningAttemptViewState(correlated, {
|
||
...initialState,
|
||
operations: [operation],
|
||
connection_attempt: {
|
||
...attempt,
|
||
status: "succeeded",
|
||
control_state: "unknown",
|
||
safe_next_action: "verify-control-read-only",
|
||
},
|
||
}),
|
||
"settling",
|
||
"the network-success/control-bootstrap handoff keeps the existing loader",
|
||
);
|
||
assert.equal(
|
||
provisioningAttemptViewState(correlated, {
|
||
...initialState,
|
||
operations: [operation],
|
||
connection_attempt: {
|
||
...attempt,
|
||
status: "failed",
|
||
control_state: "control_not_ready",
|
||
safe_next_action: "manual-recovery-required",
|
||
},
|
||
}),
|
||
"failed",
|
||
);
|
||
|
||
const connectedState = runtimeState();
|
||
connectedState.snapshot_runtime_id = presentation.snapshotRuntimeId;
|
||
assert.equal(
|
||
provisioningAttemptViewState(correlated, connectedState),
|
||
"connected",
|
||
);
|
||
assert.equal(
|
||
provisioningAttemptViewState(correlated, {
|
||
...initialState,
|
||
snapshot_runtime_id: "replacement-runtime",
|
||
}),
|
||
"retired",
|
||
);
|
||
});
|
||
|
||
test("current Bridge bootstrap stays visible over an older unresolved STOP checkpoint", () => {
|
||
const presentation = provisioningAttemptPresentation({
|
||
localPhase: "settling",
|
||
attemptId: "network-operation-current",
|
||
});
|
||
const state = durableTopologyState();
|
||
state.snapshot_runtime_id = presentation.snapshotRuntimeId;
|
||
state.connection_attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: presentation.attemptId,
|
||
connection_mode: "bridge",
|
||
status: "running",
|
||
phase: "network_applied",
|
||
control_state: "unknown",
|
||
stage: "mqtt-device-info",
|
||
public_error_code: null,
|
||
side_effect_status: "applied",
|
||
safe_next_action: "wait-for-current-attempt",
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: null,
|
||
timeline: [],
|
||
};
|
||
state.operations = [{
|
||
operation_id: presentation.attemptId,
|
||
action: "network.provision",
|
||
status: "succeeded",
|
||
idempotency_key: presentation.idempotencyKey,
|
||
}];
|
||
state.physical_command = {
|
||
status: "unresolved",
|
||
reason_code: "physical-command-reconciliation-required",
|
||
requires_reconciliation: true,
|
||
resolved_active_recovery_required: false,
|
||
automatic_replay_allowed: false,
|
||
runtime_bound: true,
|
||
reconciliation_ready: true,
|
||
observed_session_state: "scan_stopping",
|
||
active_operation_id: "older-stop-operation",
|
||
record: {
|
||
revision: 306,
|
||
operation_id: "older-stop-operation",
|
||
action: "stop",
|
||
stage: "observing",
|
||
resolution: null,
|
||
connection: {
|
||
transport_ref: presentation.deviceId,
|
||
connection_mode: "quick-connect",
|
||
target_ipv4: "192.168.56.1",
|
||
},
|
||
},
|
||
};
|
||
|
||
assert.equal(isConnectionControlBootstrapSettling(state), true);
|
||
const markup = renderProvisioningWithAttempt({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
}, presentation);
|
||
|
||
assert.match(markup, /Подтверждение подключения…/);
|
||
assert.match(markup, /Настройки переданы один раз/);
|
||
assert.match(markup, /<span>02<\/span>/);
|
||
assert.match(markup, /nodedc-activity-indicator/);
|
||
assert.doesNotMatch(markup, /Прежнее подключение не подтверждено/);
|
||
assert.doesNotMatch(markup, />Переподключиться<\/button>/);
|
||
assert.doesNotMatch(markup, />Подключить новый K1<\/button>/);
|
||
|
||
assert.equal(isConnectionControlBootstrapSettling({
|
||
...state,
|
||
connection_attempt: {
|
||
...state.connection_attempt,
|
||
status: "succeeded",
|
||
safe_next_action: "verify-control-read-only",
|
||
},
|
||
}), true);
|
||
assert.equal(isConnectionControlBootstrapSettling({
|
||
...state,
|
||
connection_attempt: {
|
||
...state.connection_attempt,
|
||
status: "failed",
|
||
control_state: "control_not_ready",
|
||
safe_next_action: "manual-recovery-required",
|
||
},
|
||
}), false);
|
||
});
|
||
|
||
test("Apply spends secret state before I/O and never stores a password in its presentation latch", () => {
|
||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||
const presentationType = sourceSlice(
|
||
source,
|
||
"export interface ProvisioningAttemptPresentation",
|
||
"export function emptyProvisioningAttemptPresentation",
|
||
);
|
||
const apply = sourceSlice(source, "const submitConnect", "const verifyAppliedNetwork");
|
||
assert.doesNotMatch(presentationType, /password\s*:/i);
|
||
assert.ok(apply.indexOf('setPassword("")') < apply.indexOf("await connect("));
|
||
assert.ok(apply.indexOf("setExplicitProvisioningDraft(null)") < apply.indexOf("await connect("));
|
||
assert.match(apply, /const attemptedPassword = password/);
|
||
assert.match(apply, /value|password/);
|
||
assert.match(source, /\|\| connectionAttemptSettling[\s\S]*?\|\| connectionAttemptFailed/);
|
||
assert.match(source, /icon=\{<ActivityIndicator \/>\}[\s\S]*?disabled/);
|
||
});
|
||
|
||
test("terminal Apply recovery is bounded and never claims that the network is ready", () => {
|
||
const presentation = provisioningAttemptPresentation({
|
||
localPhase: "settling",
|
||
attemptId: "network-operation-one",
|
||
});
|
||
const state = durableTopologyState();
|
||
state.snapshot_runtime_id = presentation.snapshotRuntimeId;
|
||
state.connection_attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: presentation.attemptId,
|
||
connection_mode: "bridge",
|
||
status: "failed",
|
||
phase: "network_applied",
|
||
control_state: "control_not_ready",
|
||
stage: "host-route-and-control-endpoint",
|
||
public_error_code: "control-bootstrap-failed",
|
||
side_effect_status: "network-applied",
|
||
safe_next_action: "manual-recovery-required",
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: OBSERVED_AT,
|
||
timeline: [],
|
||
};
|
||
state.operations = [{
|
||
operation_id: presentation.attemptId,
|
||
action: "network.provision",
|
||
status: "failed",
|
||
idempotency_key: presentation.idempotencyKey,
|
||
}];
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: [],
|
||
actions: {},
|
||
};
|
||
|
||
const markup = renderProvisioningWithAttempt({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
}, presentation);
|
||
assert.match(markup, /Сохранённое подключение/);
|
||
assert.match(markup, /Подключить заново/);
|
||
assert.equal(
|
||
buttonMarkupWithText(markup, "Подключить новый K1").length,
|
||
1,
|
||
);
|
||
assert.doesNotMatch(
|
||
markup,
|
||
/Управление не подтверждено|выбор заблокирован|Попытка настройки завершена/,
|
||
);
|
||
assert.doesNotMatch(markup, /Сеть (?:готова|настроена)|Сетевые настройки уже применены/);
|
||
assert.doesNotMatch(markup, /Пароль передан|FIELD-NET|>Применить</);
|
||
assert.doesNotMatch(markup, /aria-busy="true"/);
|
||
});
|
||
|
||
test("correlated terminal recovery resets before exposing a separate clean Scan", async () => {
|
||
const state = terminalConnectionRecoveryState({
|
||
phase: "network_not_applied",
|
||
safeNextAction: "scan-select-connect",
|
||
includeConfiguredTarget: true,
|
||
});
|
||
// A proven pre-dispatch failure must follow its exact Scan recovery instead
|
||
// of borrowing an older durable K1 observation that is still policy-visible.
|
||
state.connection_policy.recommended_action = "scan-ble";
|
||
const presentation = provisioningAttemptPresentation({
|
||
snapshotRuntimeId: state.snapshot_runtime_id,
|
||
attemptId: state.connection_attempt.attempt_id,
|
||
localPhase: "failed",
|
||
failureMessage: "Результат отправки неизвестен.",
|
||
freshStartAllowed: false,
|
||
});
|
||
const exactCorrelation = {
|
||
action: "connect",
|
||
runtimeId: state.snapshot_runtime_id,
|
||
leaseGeneration: 0,
|
||
connectionAttemptId: state.connection_attempt.attempt_id,
|
||
};
|
||
let resetCalls = 0;
|
||
let scanCalls = 0;
|
||
let resetRequest = null;
|
||
let markResetDispatched;
|
||
const resetDispatched = new Promise((resolve) => {
|
||
markResetDispatched = resolve;
|
||
});
|
||
const controller = {
|
||
...provisioningController(state),
|
||
error: "private transport exception must stay hidden",
|
||
errorDiagnostic: null,
|
||
errorCorrelation: exactCorrelation,
|
||
refresh: async () => state,
|
||
clearError() {},
|
||
selectConnectionMode: async (request) => {
|
||
resetCalls += 1;
|
||
resetRequest = request;
|
||
markResetDispatched();
|
||
return true;
|
||
},
|
||
scanWithResult: async () => {
|
||
scanCalls += 1;
|
||
return { succeeded: false };
|
||
},
|
||
};
|
||
const markup = renderProvisioningWithAttempt({
|
||
controller,
|
||
desiredMode: "bridge",
|
||
}, presentation);
|
||
|
||
assert.match(markup, /Подключение не завершено/);
|
||
assert.match(markup, /<details class="error-banner__details">/);
|
||
assert.doesNotMatch(markup, /<details[^>]*\sopen/);
|
||
assert.equal(
|
||
buttonMarkupWithText(markup, "Подключить новый K1").length,
|
||
1,
|
||
);
|
||
assert.equal(
|
||
buttonMarkupWithText(markup, "Проверить прежнее подключение").length,
|
||
0,
|
||
);
|
||
assert.doesNotMatch(markup, /Проверить состояние|>Закрыть<\/button>/);
|
||
assert.doesNotMatch(markup, /Выбрать другое|Пароль Wi‑Fi|Пароль передан|>Применить</);
|
||
assert.doesNotMatch(markup, /<span>02<\/span>|private transport exception/);
|
||
|
||
const busyMarkup = renderProvisioningWithAttempt({
|
||
controller: {
|
||
...controller,
|
||
pendingAction: "mode",
|
||
},
|
||
desiredMode: "bridge",
|
||
}, presentation);
|
||
const busyReset = buttonMarkupWithText(
|
||
busyMarkup,
|
||
"Подключить новый K1",
|
||
)[0];
|
||
assert.ok(busyReset);
|
||
assert.match(busyReset, /\bdisabled(?:=|\s|>)/);
|
||
|
||
const tree = captureProvisioningTreeWithAttempt({
|
||
controller,
|
||
desiredMode: "bridge",
|
||
}, presentation);
|
||
const errorSurface = elementByProp(
|
||
tree,
|
||
"title",
|
||
"Подключение не завершено",
|
||
);
|
||
assert.ok(errorSurface);
|
||
const resetAction = actionByLabel(
|
||
errorSurface.props.recoveryActions,
|
||
"Подключить новый K1",
|
||
);
|
||
assert.ok(resetAction);
|
||
resetAction.props.onClick();
|
||
await Promise.race([
|
||
resetDispatched,
|
||
new Promise((_, reject) => setTimeout(
|
||
() => reject(new Error("terminal recovery did not dispatch scenario reset")),
|
||
100,
|
||
)),
|
||
]);
|
||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||
assert.equal(resetCalls, 1);
|
||
assert.deepEqual(
|
||
{
|
||
connection_mode: resetRequest.connection_mode,
|
||
expected_revision: resetRequest.expected_revision,
|
||
reset_scenario: resetRequest.reset_scenario,
|
||
},
|
||
{
|
||
connection_mode: "bridge",
|
||
expected_revision: state.desired_connection_mode_revision,
|
||
reset_scenario: true,
|
||
},
|
||
);
|
||
assert.equal(typeof resetRequest.reset_id, "string");
|
||
assert.ok(resetRequest.reset_id.length > 0);
|
||
assert.equal(scanCalls, 0);
|
||
|
||
const cleanState = structuredClone(state);
|
||
cleanState.connection_attempt = null;
|
||
cleanState.operations = [];
|
||
cleanState.desired_connection_mode_revision += 1;
|
||
cleanState.connection_scenario_reset = {
|
||
reset_id: resetRequest.reset_id,
|
||
request_revision: state.desired_connection_mode_revision,
|
||
revision: cleanState.desired_connection_mode_revision,
|
||
desired_mode: "bridge",
|
||
active: false,
|
||
settled_by_discovery_generation: cleanState.ble_discovery_generation,
|
||
local_session_closed: true,
|
||
previous_device_may_continue_scanning: false,
|
||
physical_disposition: null,
|
||
network_disposition: null,
|
||
device_command_performed: false,
|
||
network_write_performed: false,
|
||
automatic_scan: false,
|
||
operation_sequence: 9,
|
||
};
|
||
const cleanMarkup = renderProvisioning({
|
||
controller: provisioningController(cleanState),
|
||
desiredMode: "bridge",
|
||
});
|
||
const cleanScan = buttonMarkupWithText(cleanMarkup, "Найти по Bluetooth");
|
||
assert.equal(cleanScan.length, 1);
|
||
assert.doesNotMatch(cleanScan[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(
|
||
buttonMarkupWithText(cleanMarkup, "Подключить новый K1").length,
|
||
0,
|
||
);
|
||
assert.equal(scanCalls, 0);
|
||
|
||
const deniedState = terminalConnectionRecoveryState({
|
||
phase: "network_not_applied",
|
||
safeNextAction: "scan-select-connect",
|
||
includeConfiguredTarget: false,
|
||
includeScan: false,
|
||
});
|
||
const deniedMarkup = renderProvisioningWithAttempt({
|
||
controller: {
|
||
...provisioningController(deniedState),
|
||
error: "denied",
|
||
errorCorrelation: {
|
||
...exactCorrelation,
|
||
runtimeId: deniedState.snapshot_runtime_id,
|
||
connectionAttemptId: deniedState.connection_attempt.attempt_id,
|
||
},
|
||
refresh: async () => deniedState,
|
||
clearError() {},
|
||
},
|
||
desiredMode: "bridge",
|
||
}, {
|
||
...presentation,
|
||
snapshotRuntimeId: deniedState.snapshot_runtime_id,
|
||
attemptId: deniedState.connection_attempt.attempt_id,
|
||
});
|
||
assert.equal(
|
||
buttonMarkupWithText(deniedMarkup, "Подключить новый K1").length,
|
||
1,
|
||
);
|
||
|
||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||
const search = sourceSlice(source, "const repeatDeviceScan", "const submitConnect");
|
||
const actions = sourceSlice(
|
||
source,
|
||
"const connectionRecoveryActions",
|
||
"return (",
|
||
);
|
||
assert.equal((search.match(/scanWithResult\(/g) ?? []).length, 1);
|
||
assert.match(
|
||
search,
|
||
/isConnectionPolicyActionAllowedCurrent\("scan-ble"\)/,
|
||
);
|
||
assert.match(
|
||
actions,
|
||
/changeDesiredConnectionMode\(connectionMode\)/,
|
||
);
|
||
assert.doesNotMatch(
|
||
actions,
|
||
/repeatDeviceScan\(|scanWithResult\(|xgridsK1Api|\.scanBle\(/,
|
||
);
|
||
});
|
||
|
||
test("unknown and cold durable recovery offer one reconnect and one new-device path", () => {
|
||
const state = terminalConnectionRecoveryState();
|
||
const target = recommendedConnectionRecoveryObservationTarget(state);
|
||
assert.equal(target?.action, "observe-configured-device-network");
|
||
const presentation = provisioningAttemptPresentation({
|
||
snapshotRuntimeId: state.snapshot_runtime_id,
|
||
attemptId: state.connection_attempt.attempt_id,
|
||
localPhase: "failed",
|
||
failureMessage: "Результат отправки неизвестен.",
|
||
freshStartAllowed: false,
|
||
});
|
||
const controller = {
|
||
...provisioningController(state),
|
||
error: "hidden",
|
||
errorDiagnostic: null,
|
||
errorCorrelation: {
|
||
action: "connect",
|
||
runtimeId: state.snapshot_runtime_id,
|
||
leaseGeneration: 0,
|
||
connectionAttemptId: state.connection_attempt.attempt_id,
|
||
},
|
||
refresh: async () => state,
|
||
clearError() {},
|
||
getConnectionRecoveryObservationTarget: () => target,
|
||
};
|
||
const markup = renderProvisioningWithAttempt({
|
||
controller,
|
||
desiredMode: "bridge",
|
||
}, presentation);
|
||
const reconnectOffset = markup.indexOf("Переподключиться");
|
||
const newDeviceOffset = markup.indexOf("Подключить новый K1");
|
||
assert.ok(reconnectOffset > 0);
|
||
assert.ok(newDeviceOffset > reconnectOffset);
|
||
assert.match(markup, /Результат применения настроек сети не подтверждён/);
|
||
assert.doesNotMatch(markup, /Настройки сети не применены/);
|
||
assert.doesNotMatch(markup, /Сеть (?:готова|настроена)|>Применить</);
|
||
assert.doesNotMatch(markup, /<span>02<\/span>/);
|
||
|
||
const coldState = structuredClone(state);
|
||
coldState.connection_attempt = null;
|
||
const coldTarget = recommendedConnectionRecoveryObservationTarget(coldState);
|
||
const coldMarkup = renderProvisioning({
|
||
controller: {
|
||
...provisioningController(coldState),
|
||
getConnectionRecoveryObservationTarget: () => coldTarget,
|
||
},
|
||
desiredMode: "bridge",
|
||
});
|
||
const coldReconnectOffset = coldMarkup.indexOf("Переподключиться");
|
||
const coldNewDeviceOffset = coldMarkup.indexOf("Подключить новый K1");
|
||
assert.ok(coldReconnectOffset > 0);
|
||
assert.ok(coldNewDeviceOffset > coldReconnectOffset);
|
||
assert.doesNotMatch(coldMarkup, /Локальная операция завершилась ошибкой/);
|
||
assert.doesNotMatch(coldMarkup, /<span>02<\/span>|>Применить</);
|
||
|
||
const coldQuickState = structuredClone(coldState);
|
||
coldQuickState.connection_policy.actions[
|
||
"observe-configured-device-network"
|
||
].required_connection_mode = "quick-connect";
|
||
const coldQuickTarget = recommendedConnectionRecoveryObservationTarget(
|
||
coldQuickState,
|
||
);
|
||
const coldQuickMarkup = renderProvisioning({
|
||
controller: {
|
||
...provisioningController(coldQuickState),
|
||
getConnectionRecoveryObservationTarget: () => coldQuickTarget,
|
||
},
|
||
desiredMode: "quick-connect",
|
||
});
|
||
const coldQuickReconnect = buttonMarkupWithText(
|
||
coldQuickMarkup,
|
||
"Переподключиться",
|
||
)[0];
|
||
assert.ok(coldQuickReconnect);
|
||
assert.doesNotMatch(coldQuickReconnect, /\bdisabled(?:=|\s|>)/);
|
||
assert.equal(
|
||
buttonMarkupWithText(coldQuickMarkup, "Подключить новый K1").length,
|
||
1,
|
||
);
|
||
assert.match(coldQuickMarkup, /Quick Connect/);
|
||
|
||
const quickTargetInsideBridgeMarkup = renderProvisioning({
|
||
controller: {
|
||
...provisioningController(coldQuickState),
|
||
getConnectionRecoveryObservationTarget: () => coldQuickTarget,
|
||
},
|
||
desiredMode: "bridge",
|
||
});
|
||
assert.equal(
|
||
buttonMarkupWithText(
|
||
quickTargetInsideBridgeMarkup,
|
||
"Переподключиться",
|
||
).length,
|
||
0,
|
||
);
|
||
assert.equal(
|
||
buttonMarkupWithText(
|
||
quickTargetInsideBridgeMarkup,
|
||
"Найти по Bluetooth",
|
||
).length,
|
||
1,
|
||
);
|
||
assert.doesNotMatch(
|
||
quickTargetInsideBridgeMarkup,
|
||
/Сохранённое подключение/,
|
||
);
|
||
|
||
const unrelatedMarkup = renderProvisioningWithAttempt({
|
||
controller: {
|
||
...controller,
|
||
errorCorrelation: { ...controller.errorCorrelation, action: "scan" },
|
||
},
|
||
desiredMode: "bridge",
|
||
}, presentation);
|
||
assert.doesNotMatch(unrelatedMarkup, /op-b7ea404d/);
|
||
assert.match(unrelatedMarkup, /Сохранённое подключение/);
|
||
});
|
||
|
||
test("saved Bridge reconnect stays hidden in a fresh Quick Connect draft", () => {
|
||
const state = terminalConnectionRecoveryState();
|
||
state.connection_attempt = null;
|
||
const bridgeTarget = recommendedConnectionRecoveryObservationTarget(state);
|
||
assert.equal(bridgeTarget?.connectionMode, "bridge");
|
||
const controller = {
|
||
...provisioningController(state),
|
||
getConnectionRecoveryObservationTarget: () => bridgeTarget,
|
||
};
|
||
|
||
const quickConnectMarkup = renderProvisioning({
|
||
controller,
|
||
desiredMode: "quick-connect",
|
||
});
|
||
assert.equal(
|
||
buttonMarkupWithText(quickConnectMarkup, "Переподключиться").length,
|
||
0,
|
||
);
|
||
assert.equal(
|
||
buttonMarkupWithText(quickConnectMarkup, "Найти по Bluetooth").length,
|
||
1,
|
||
);
|
||
assert.doesNotMatch(quickConnectMarkup, /Сохранённое подключение/);
|
||
|
||
const bridgeMarkup = renderProvisioning({
|
||
controller,
|
||
desiredMode: "bridge",
|
||
});
|
||
assert.equal(
|
||
buttonMarkupWithText(bridgeMarkup, "Переподключиться").length,
|
||
1,
|
||
);
|
||
assert.match(bridgeMarkup, /Сохранённое подключение/);
|
||
});
|
||
|
||
test("read-only reconnect keeps its card and loader across a transient projection", async () => {
|
||
const initialState = terminalConnectionRecoveryState();
|
||
initialState.connection_attempt = null;
|
||
let currentState = initialState;
|
||
let resolveVerification;
|
||
let markVerificationStarted;
|
||
const verificationStarted = new Promise((resolve) => {
|
||
markVerificationStarted = resolve;
|
||
});
|
||
const controller = {
|
||
...provisioningController(initialState),
|
||
verifyConnection: async () => {
|
||
markVerificationStarted();
|
||
return new Promise((resolve) => {
|
||
resolveVerification = resolve;
|
||
});
|
||
},
|
||
getConnectionRecoveryObservationTarget: () =>
|
||
recommendedConnectionRecoveryObservationTarget(currentState),
|
||
isSnapshotRuntimeCurrent: (runtimeId) =>
|
||
runtimeId === currentState.snapshot_runtime_id,
|
||
};
|
||
const props = () => ({
|
||
controller: { ...controller, state: currentState },
|
||
desiredMode: "bridge",
|
||
});
|
||
const harness = createStatefulProvisioningHarness(props());
|
||
|
||
try {
|
||
let tree = harness.render(props());
|
||
const reconnect = actionByLabel(tree, "Переподключиться");
|
||
assert.ok(reconnect);
|
||
reconnect.props.onClick();
|
||
await Promise.race([
|
||
verificationStarted,
|
||
new Promise((_, reject) => setTimeout(
|
||
() => reject(new Error("read-only reconnect did not dispatch")),
|
||
100,
|
||
)),
|
||
]);
|
||
|
||
const runningVerificationState = structuredClone(initialState);
|
||
runningVerificationState.connection_attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: "verify-saved-k1-running",
|
||
connection_mode: "bridge",
|
||
status: "running",
|
||
phase: "network_applied",
|
||
control_state: "unknown",
|
||
stage: "host-route-and-control-endpoint",
|
||
public_error_code: null,
|
||
side_effect_status: "network-applied",
|
||
safe_next_action: "wait-for-current-attempt",
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: null,
|
||
timeline: [],
|
||
};
|
||
currentState = runningVerificationState;
|
||
tree = harness.render(props());
|
||
const runningMarkup = renderToStaticMarkup(tree);
|
||
const runningReconnect = buttonMarkupWithText(
|
||
runningMarkup,
|
||
"Переподключение…",
|
||
);
|
||
assert.equal(runningReconnect.length, 1);
|
||
assert.match(runningReconnect[0], /disabled/);
|
||
assert.match(runningReconnect[0], /aria-busy="true"/);
|
||
assert.match(runningReconnect[0], /nodedc-activity-indicator/);
|
||
assert.match(runningMarkup, /Сохранённое подключение/);
|
||
assert.doesNotMatch(runningMarkup, /Команда настройки завершена/);
|
||
assert.doesNotMatch(runningMarkup, /Подтверждение управления/);
|
||
|
||
const transientState = structuredClone(initialState);
|
||
transientState.connection_policy.allowed_actions =
|
||
transientState.connection_policy.allowed_actions.filter(
|
||
(action) => action !== "observe-configured-device-network",
|
||
);
|
||
delete transientState.connection_policy.actions[
|
||
"observe-configured-device-network"
|
||
];
|
||
currentState = transientState;
|
||
tree = harness.render(props());
|
||
const pendingMarkup = renderToStaticMarkup(tree);
|
||
const pendingReconnect = buttonMarkupWithText(
|
||
pendingMarkup,
|
||
"Переподключение…",
|
||
);
|
||
assert.equal(pendingReconnect.length, 1);
|
||
assert.match(pendingReconnect[0], /disabled/);
|
||
assert.match(pendingReconnect[0], /aria-busy="true"/);
|
||
assert.match(pendingReconnect[0], /nodedc-activity-indicator/);
|
||
assert.match(pendingMarkup, /Сохранённое подключение/);
|
||
assert.doesNotMatch(pendingMarkup, /connection-action-progress/);
|
||
assert.equal(
|
||
harness.stateValue(emptyReadOnlyReconnectPresentation)?.kind,
|
||
"connection",
|
||
);
|
||
|
||
currentState = initialState;
|
||
resolveVerification({
|
||
succeeded: false,
|
||
reconciliationCompleted: false,
|
||
observedState: initialState,
|
||
});
|
||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||
tree = harness.render(props());
|
||
const failedMarkup = renderToStaticMarkup(tree);
|
||
const retry = buttonMarkupWithText(failedMarkup, "Переподключиться");
|
||
assert.equal(retry.length, 1);
|
||
assert.doesNotMatch(retry[0], /disabled|aria-busy="true"/);
|
||
assert.match(failedMarkup, /Проверьте питание и сеть, затем повторите проверку/);
|
||
assert.equal(harness.stateValue(emptyReadOnlyReconnectPresentation), null);
|
||
} finally {
|
||
harness.dispose();
|
||
}
|
||
});
|
||
|
||
test("cold saved target keeps fast reconnect after a reset and reload", async () => {
|
||
const state = terminalConnectionRecoveryState();
|
||
state.connection_attempt = null;
|
||
const target = recommendedConnectionRecoveryObservationTarget(state);
|
||
assert.ok(target);
|
||
const resetRequests = [];
|
||
let scanCalls = 0;
|
||
let verifyCalls = 0;
|
||
let connectCalls = 0;
|
||
let markResetDispatched;
|
||
const resetDispatched = new Promise((resolve) => {
|
||
markResetDispatched = resolve;
|
||
});
|
||
const controller = {
|
||
...provisioningController(state),
|
||
getConnectionRecoveryObservationTarget: () => target,
|
||
selectConnectionMode: async (request) => {
|
||
resetRequests.push(request);
|
||
markResetDispatched();
|
||
return true;
|
||
},
|
||
scanWithResult: async () => {
|
||
scanCalls += 1;
|
||
return { succeeded: false };
|
||
},
|
||
verifyConnection: async () => {
|
||
verifyCalls += 1;
|
||
return { succeeded: false };
|
||
},
|
||
connect: async () => {
|
||
connectCalls += 1;
|
||
return { succeeded: false };
|
||
},
|
||
};
|
||
const props = {
|
||
controller,
|
||
desiredMode: "bridge",
|
||
};
|
||
const markup = renderProvisioning(props);
|
||
const reconnect = buttonMarkupWithText(markup, "Переподключиться");
|
||
const connectNew = buttonMarkupWithText(markup, "Подключить новый K1");
|
||
assert.equal(reconnect.length, 1);
|
||
assert.equal(connectNew.length, 1);
|
||
assert.doesNotMatch(reconnect[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.doesNotMatch(connectNew[0], /\bdisabled(?:=|\s|>)/);
|
||
|
||
const tree = captureProvisioningTree(props);
|
||
const resetAction = actionByLabel(tree, "Подключить новый K1");
|
||
assert.ok(resetAction);
|
||
resetAction.props.onClick();
|
||
await Promise.race([
|
||
resetDispatched,
|
||
new Promise((_, reject) => setTimeout(
|
||
() => reject(new Error("cold new-device path did not dispatch reset")),
|
||
100,
|
||
)),
|
||
]);
|
||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||
|
||
assert.equal(resetRequests.length, 1);
|
||
assert.deepEqual(
|
||
{
|
||
connection_mode: resetRequests[0].connection_mode,
|
||
expected_revision: resetRequests[0].expected_revision,
|
||
reset_scenario: resetRequests[0].reset_scenario,
|
||
},
|
||
{
|
||
connection_mode: "bridge",
|
||
expected_revision: state.desired_connection_mode_revision,
|
||
reset_scenario: true,
|
||
},
|
||
);
|
||
assert.equal(typeof resetRequests[0].reset_id, "string");
|
||
assert.ok(resetRequests[0].reset_id.length > 0);
|
||
assert.equal(scanCalls, 0);
|
||
assert.equal(verifyCalls, 0);
|
||
assert.equal(connectCalls, 0);
|
||
|
||
const reloaded = structuredClone(state);
|
||
reloaded.desired_connection_mode_revision += 1;
|
||
reloaded.connection_scenario_reset = {
|
||
reset_id: resetRequests[0].reset_id,
|
||
request_revision: state.desired_connection_mode_revision,
|
||
revision: reloaded.desired_connection_mode_revision,
|
||
desired_mode: "bridge",
|
||
active: false,
|
||
settled_by_discovery_generation: reloaded.ble_discovery_generation,
|
||
local_session_closed: true,
|
||
previous_device_may_continue_scanning: false,
|
||
physical_disposition: null,
|
||
network_disposition: null,
|
||
device_command_performed: false,
|
||
network_write_performed: false,
|
||
automatic_scan: false,
|
||
operation_sequence: 31,
|
||
};
|
||
const reloadedMarkup = renderProvisioning({
|
||
controller: {
|
||
...provisioningController(reloaded),
|
||
getConnectionRecoveryObservationTarget: () =>
|
||
recommendedConnectionRecoveryObservationTarget(reloaded),
|
||
},
|
||
desiredMode: "bridge",
|
||
});
|
||
const reloadedReconnect = buttonMarkupWithText(
|
||
reloadedMarkup,
|
||
"Переподключиться",
|
||
);
|
||
assert.equal(reloadedReconnect.length, 1);
|
||
assert.doesNotMatch(reloadedReconnect[0], /\bdisabled(?:=|\s|>)/);
|
||
const cleanScan = buttonMarkupWithText(reloadedMarkup, "Найти по Bluetooth");
|
||
assert.equal(cleanScan.length, 1);
|
||
assert.doesNotMatch(cleanScan[0], /\bdisabled(?:=|\s|>)/);
|
||
assert.match(reloadedMarkup, /Сохранённое подключение/);
|
||
assert.match(reloadedMarkup, /Можно переподключиться/);
|
||
assert.doesNotMatch(reloadedMarkup, /Подключить новый K1/);
|
||
});
|
||
|
||
test("a current post-reset failed or unknown attempt outranks the durable reset marker after reload", () => {
|
||
const withSettledReset = (state) => {
|
||
state.desired_connection_mode_revision = 1;
|
||
state.connection_scenario_reset = {
|
||
reset_id: "reset-before-new-attempt",
|
||
request_revision: 0,
|
||
revision: 1,
|
||
desired_mode: "bridge",
|
||
active: false,
|
||
settled_by_discovery_generation: state.ble_discovery_generation,
|
||
local_session_closed: true,
|
||
previous_device_may_continue_scanning: false,
|
||
physical_disposition: null,
|
||
network_disposition: null,
|
||
device_command_performed: false,
|
||
network_write_performed: false,
|
||
automatic_scan: false,
|
||
// Operation sequence is per operation, not a global chronology. The new
|
||
// post-reset operation below can therefore legitimately have 3 < 6.
|
||
operation_sequence: 6,
|
||
};
|
||
state.operations = [{
|
||
operation_id: state.connection_attempt.attempt_id,
|
||
action: "network.provision",
|
||
status: "failed",
|
||
sequence: 3,
|
||
idempotency_key: "post-reset-new-network-attempt",
|
||
}];
|
||
return state;
|
||
};
|
||
|
||
const unknown = withSettledReset(terminalConnectionRecoveryState({
|
||
phase: "network_outcome_unknown",
|
||
safeNextAction: "verify-control-read-only",
|
||
}));
|
||
const unknownTarget = recommendedConnectionRecoveryObservationTarget(unknown);
|
||
assert.ok(unknownTarget);
|
||
const unknownReloadMarkup = renderProvisioning({
|
||
controller: {
|
||
...provisioningController(unknown),
|
||
getConnectionRecoveryObservationTarget: () => unknownTarget,
|
||
},
|
||
desiredMode: "bridge",
|
||
});
|
||
assert.match(unknownReloadMarkup, /Сохранённое подключение/);
|
||
assert.equal(
|
||
buttonMarkupWithText(unknownReloadMarkup, "Переподключиться").length,
|
||
1,
|
||
);
|
||
assert.equal(
|
||
buttonMarkupWithText(unknownReloadMarkup, "Подключить новый K1").length,
|
||
1,
|
||
);
|
||
assert.equal(
|
||
buttonMarkupWithText(unknownReloadMarkup, "Найти по Bluetooth").length,
|
||
0,
|
||
);
|
||
|
||
const failed = withSettledReset(terminalConnectionRecoveryState({
|
||
phase: "network_applied",
|
||
safeNextAction: "manual-recovery-required",
|
||
}));
|
||
failed.connection_attempt.control_state = "control_not_ready";
|
||
const failedReloadMarkup = renderProvisioning({
|
||
controller: provisioningController(failed),
|
||
desiredMode: "bridge",
|
||
});
|
||
assert.match(failedReloadMarkup, /Сохранённое подключение/);
|
||
assert.match(failedReloadMarkup, /Можно переподключиться/);
|
||
assert.equal(
|
||
buttonMarkupWithText(failedReloadMarkup, "Переподключиться").length,
|
||
1,
|
||
);
|
||
assert.equal(
|
||
buttonMarkupWithText(failedReloadMarkup, "Подключить новый K1").length,
|
||
1,
|
||
);
|
||
assert.doesNotMatch(
|
||
failedReloadMarkup,
|
||
/Управление не подтверждено|выбор заблокирован|Новый выбор временно заблокирован/,
|
||
);
|
||
});
|
||
|
||
test("Apply never owns hidden refresh or an automatic continuation", () => {
|
||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||
const apply = sourceSlice(source, "const submitConnect", "const verifyAppliedNetwork");
|
||
assert.doesNotMatch(
|
||
apply,
|
||
/scanWithResult\(|candidateRefresh|verifyConnection\(|void submitConnect/,
|
||
);
|
||
});
|
||
|
||
test("stale authority is explicit and never settles a hidden refresh", () => {
|
||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||
const apply = sourceSlice(source, "const submitConnect", "const verifyAppliedNetwork");
|
||
assert.doesNotMatch(apply, /candidateRefresh|scanWithResult\(/);
|
||
assert.match(source, /устарел|stale/i);
|
||
});
|
||
|
||
test("a same-runtime authority drift keeps one neutral settling surface", () => {
|
||
const state = durableTopologyState();
|
||
const markup = renderProvisioning({
|
||
controller: {
|
||
...provisioningController(state),
|
||
pendingAction: "verify",
|
||
// A synchronous authority read has already moved beyond the click-owned
|
||
// fence while the controller is still settling its old I/O.
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
isConnectionActionAuthorityCurrent: () => false,
|
||
},
|
||
desiredMode: "bridge",
|
||
});
|
||
|
||
assert.match(markup, /<span>01<\/span>/);
|
||
assert.doesNotMatch(markup, /<span>02<\/span>|<span>03<\/span>/);
|
||
assert.doesNotMatch(markup, /Завершаем ранее начатое действие…/);
|
||
assert.equal((markup.match(/class="nodedc-activity-indicator"/g) ?? []).length, 0);
|
||
assert.doesNotMatch(markup, /Проверяем сохранённое подключение…/);
|
||
assert.doesNotMatch(markup, /Подключаемся к сохранённому K1…/);
|
||
});
|
||
|
||
test("a stale reopen settlement cannot own the current wizard surface", () => {
|
||
const state = durableTopologyState();
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["scan-ble"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
const markup = renderProvisioning({
|
||
controller: {
|
||
...provisioningController(state),
|
||
pendingAction: "reopen",
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
isConnectionActionAuthorityCurrent: () => false,
|
||
},
|
||
desiredMode: "bridge",
|
||
});
|
||
|
||
assert.match(markup, /<span>01<\/span>/);
|
||
assert.doesNotMatch(markup, /Подключение…|aria-busy="true"/);
|
||
assert.doesNotMatch(markup, /<span>02<\/span>|<span>03<\/span>/);
|
||
assert.match(markup, />Найти по Bluetooth<\/button>/);
|
||
const modeToggle = markup.match(
|
||
/<button[^>]*aria-label="Способ подключения"[^>]*>/,
|
||
)?.[0];
|
||
assert.ok(modeToggle);
|
||
assert.doesNotMatch(modeToggle, /\bdisabled(?:=|\s|>)/);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
test("network step 02 is disclosed only by a selected device or a real network intent", () => {
|
||
assert.equal(shouldRevealProvisioningNetworkStep({
|
||
deviceExplicitlySelectedOrAdmitted: false,
|
||
networkIntentStarted: false,
|
||
}), false);
|
||
assert.equal(shouldRevealProvisioningNetworkStep({
|
||
deviceExplicitlySelectedOrAdmitted: true,
|
||
networkIntentStarted: false,
|
||
}), true);
|
||
assert.equal(shouldRevealProvisioningNetworkStep({
|
||
deviceExplicitlySelectedOrAdmitted: false,
|
||
networkIntentStarted: true,
|
||
}), true);
|
||
});
|
||
|
||
test("a current-authority Bluetooth scan owns Step 01 and no network step", () => {
|
||
const state = reopenedPhysicalState();
|
||
state.physical_command = null;
|
||
state.connection_policy.actions["provision-fresh-device"] = {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: null,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
};
|
||
state.connection_policy.allowed_actions.push("provision-fresh-device");
|
||
const authority = reopenedPhysicalAuthority();
|
||
const markup = renderProvisioningWithCurrentPendingAction({
|
||
controller: {
|
||
...provisioningController(state),
|
||
pendingAction: "scan",
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
isConnectionActionAuthorityCurrent: (candidate) =>
|
||
connectionActionAuthorityMatches(candidate, authority),
|
||
},
|
||
desiredMode: "bridge",
|
||
});
|
||
|
||
assert.match(markup, /<span>01<\/span>/);
|
||
assert.match(markup, /Поиск Bluetooth · до 20 с/);
|
||
assert.equal(
|
||
(markup.match(/class="nodedc-activity-indicator"/g) ?? []).length,
|
||
1,
|
||
);
|
||
assert.doesNotMatch(markup, /Найдено: 0|Совпадений нет|Результатов:/);
|
||
assert.equal(
|
||
buttonMarkupWithText(markup, "Подключить новый K1").length,
|
||
0,
|
||
);
|
||
assert.doesNotMatch(markup, /<span>02<\/span>|<span>03<\/span>/);
|
||
assert.doesNotMatch(markup, /Настройка сети|Название общей сети Wi‑Fi/);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
test("stale raw Bluetooth results stay hidden until an explicit search", () => {
|
||
const state = durableTopologyState();
|
||
state.semantic_topology_store = {
|
||
...state.semantic_topology_store,
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
record: null,
|
||
};
|
||
state.devices = [{
|
||
device_id: "replacement-k1",
|
||
name: "Replacement K1",
|
||
rssi: -44,
|
||
connectable: true,
|
||
likely_k1: true,
|
||
}];
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["scan-ble", "provision-fresh-device"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
"provision-fresh-device": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "fresh-scan",
|
||
required_transport_ref: null,
|
||
required_connection_mode: "bridge",
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
const markup = renderProvisioning({
|
||
controller: {
|
||
...provisioningController(state),
|
||
// The raw promise is still settling, but its click authority has already
|
||
// been retired by the completed discovery-generation transition.
|
||
pendingAction: "scan",
|
||
isSnapshotRuntimeCurrent: () => true,
|
||
isConnectionActionAuthorityCurrent: () => false,
|
||
},
|
||
desiredMode: "bridge",
|
||
});
|
||
|
||
assert.match(markup, /<span>01<\/span>/);
|
||
assert.doesNotMatch(markup, /Replacement K1/);
|
||
assert.match(markup, />Найти по Bluetooth<\/button>/);
|
||
assert.doesNotMatch(markup, /aria-busy="true"/);
|
||
assert.doesNotMatch(markup, /<span>02<\/span>|<span>03<\/span>/);
|
||
assert.doesNotMatch(markup, /Завершаем предыдущее действие|Завершаем ранее начатое действие/);
|
||
assert.doesNotMatch(markup, /Настройка сети|Сначала найдите и выберите K1/);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
test("a completed search never adopts another tab's discovery generation", () => {
|
||
const state = durableTopologyState();
|
||
state.ble_discovery_generation = 8;
|
||
state.semantic_topology_store = {
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: null,
|
||
};
|
||
state.devices = [{
|
||
device_id: "generation-eight-result",
|
||
name: "Generation eight",
|
||
rssi: -46,
|
||
connectable: true,
|
||
likely_k1: true,
|
||
}];
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["scan-ble"],
|
||
actions: {
|
||
"scan-ble": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "none",
|
||
required_transport_ref: null,
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
};
|
||
|
||
const markup = renderProvisioningAfterSearch({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
}, { completedDiscoveryGeneration: 7 });
|
||
|
||
assert.doesNotMatch(markup, /generation-eight-result|Generation eight/);
|
||
assert.match(markup, /Найти по Bluetooth/);
|
||
assert.doesNotMatch(markup, /Результатов:|Совпадений нет/);
|
||
assertCanonicalConnectionCopy(markup);
|
||
});
|
||
|
||
test("change-network opens credentials immediately and Apply does not refresh", () => {
|
||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||
const apply = sourceSlice(source, "const submitConnect", "const verifyAppliedNetwork");
|
||
assert.doesNotMatch(apply, /scanWithResult\(|verifyConnection\(|candidateRefresh/);
|
||
assert.equal((source.match(/buttonLabel:\s*"Применить"/g) ?? []).length, 3);
|
||
});
|
||
|
||
test("Step-2 credentials and Apply share one canonical field stack", () => {
|
||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||
const styles = readFileSync(provisioningStylesUrl, "utf8");
|
||
const form = sourceSlice(
|
||
source,
|
||
"const provisioningDraftContent = (",
|
||
"return (",
|
||
);
|
||
|
||
assert.match(
|
||
form,
|
||
/:\s*\(\s*<div className="field-stack"[\s\S]*?label="Пароль Wi‑Fi"[\s\S]*?\{applyAction\}\s*<\/div>/,
|
||
);
|
||
assert.match(form, /disabled=\{provisioningFieldsDisabled\}/);
|
||
assert.match(form, /value=\{connectionAttemptOwnsDraft \? "" : password\}/);
|
||
assert.match(
|
||
styles,
|
||
/\.field-stack,\s*\.session-form\s*\{\s*display:\s*grid;\s*gap:\s*0\.85rem;/,
|
||
);
|
||
});
|
||
|
||
test("applied-network recovery spends the old intent and admits only explicit server-authorized paths", () => {
|
||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||
const apply = sourceSlice(source, "const submitConnect", "const verifyAppliedNetwork");
|
||
const recoveryState = sourceSlice(
|
||
source,
|
||
"const appliedNetworkAttempt",
|
||
"const localProvisioningPrerequisitesReady",
|
||
);
|
||
const appliedRecoveryTarget = sourceSlice(
|
||
source,
|
||
"const appliedNetworkRecoveryTarget",
|
||
"const networkRecoveryModeLabel",
|
||
);
|
||
const connectAuthority = sourceSlice(
|
||
source,
|
||
"const canConnect",
|
||
"const scanAllowedByPolicy",
|
||
);
|
||
const scanAuthority = sourceSlice(
|
||
source,
|
||
"const backendScanAllowed",
|
||
"const physicalStopRecoverySettling",
|
||
);
|
||
const search = sourceSlice(source, "const repeatDeviceScan", "const submitConnect");
|
||
const recovery = sourceSlice(
|
||
source,
|
||
"const verifyAppliedNetwork",
|
||
"const verifyConnectionRecoveryTarget",
|
||
);
|
||
const prepare = sourceSlice(
|
||
source,
|
||
"const prepareReconfiguration",
|
||
"const changeDesiredConnectionMode",
|
||
);
|
||
const mode = sourceSlice(
|
||
source,
|
||
"const changeDesiredConnectionMode",
|
||
"const selectFreshDevice",
|
||
);
|
||
const selection = sourceSlice(
|
||
source,
|
||
"const selectFreshDevice",
|
||
"// Only an explicit public search",
|
||
);
|
||
const recoveryEscape = sourceSlice(
|
||
source,
|
||
"const recoverBySelectingAnotherDevice",
|
||
"const attemptButtonLabel",
|
||
);
|
||
|
||
assert.doesNotMatch(apply, /verifyConnection\(/);
|
||
assert.match(recoveryState, /state\?\.connection_attempt\?\.phase === "network_applied"/);
|
||
assert.match(
|
||
recoveryState,
|
||
/connectionAttemptOwnsAppliedNetworkRecovery\(appliedNetworkAttempt\)/,
|
||
);
|
||
assert.match(
|
||
recoveryState,
|
||
/`\$\{snapshotRuntimeId\}:\$\{appliedNetworkAttempt\.attempt_id\}`/,
|
||
);
|
||
assert.match(
|
||
recoveryState,
|
||
/escapedAppliedAttemptKey === appliedAttemptRecoveryKey[\s\S]*?\|\| appliedRecoveryReconfigurationPrepared/,
|
||
);
|
||
assert.match(
|
||
recoveryState,
|
||
/connectionAttemptOwnsAppliedNetworkRecovery\(appliedNetworkAttempt\)[\s\S]*?&& !appliedRecoveryExplicitlyEscaped/,
|
||
);
|
||
assert.match(recoveryState, /const networkRecoveryRequired = unresolvedAppliedAttempt !== null/);
|
||
assert.match(
|
||
recoveryState,
|
||
/const appliedControlSettlementPending = Boolean\([\s\S]*?\["accepted", "running"\]\.includes\(unresolvedAppliedAttempt\.status\)[\s\S]*?safe_next_action === "wait-for-current-attempt"/,
|
||
);
|
||
assert.doesNotMatch(
|
||
appliedRecoveryTarget,
|
||
/\bconnectionMode\b|selectedDeviceId|selectedTarget/,
|
||
);
|
||
assert.match(
|
||
appliedRecoveryTarget,
|
||
/serverBoundAppliedNetworkObservationTarget\(\s*state,\s*unresolvedAppliedAttempt\.connection_mode/,
|
||
);
|
||
|
||
assert.match(connectAuthority, /!unresolvedAppliedAttempt/);
|
||
assert.match(scanAuthority, /!networkRecoveryRequired/);
|
||
assert.match(apply, /\|\| unresolvedAppliedAttempt/);
|
||
assert.match(apply, /provisioningIntentKey\(null\)/);
|
||
assert.match(mode, /if \(modeResetInFlight\) return/);
|
||
assert.match(mode, /reset_scenario: true/);
|
||
assert.match(selection, /networkRecoveryRequired\s*\|\|/);
|
||
assert.doesNotMatch(
|
||
source,
|
||
/powerConfirmed|powerConfirmationEpoch|resetPowerConfirmation|Питание включено|title="Питание"/,
|
||
);
|
||
assert.match(source, /value=\{connectionMode\}[\s\S]*?onChange=\{\(value\) => void changeDesiredConnectionMode\(value\)\}/);
|
||
assert.match(
|
||
sourceSlice(source, "<Select\n label=\"Способ подключения\"", "<Button"),
|
||
/disabled=\{modeResetInFlight\}/,
|
||
);
|
||
|
||
assert.match(recovery, /appliedNetworkRecoveryTarget/);
|
||
assert.match(recovery, /appliedControlSettlementPending/);
|
||
assert.equal((recovery.match(/await verifyConnection\(/g) ?? []).length, 1);
|
||
assert.doesNotMatch(
|
||
recovery,
|
||
/selectedDeviceId|selectedTarget|selectedDeviceSnapshot|provisioningCandidateById/,
|
||
);
|
||
|
||
assert.match(
|
||
source,
|
||
/const canPrepareRecoverySelectDevice = Boolean\(\s*unresolvedAppliedAttempt\?\.connection_mode === "bridge"[\s\S]*?selectDeviceAllowedByPolicy/,
|
||
);
|
||
assert.match(
|
||
source,
|
||
/const canPrepareRecoverySelectDevice = Boolean\([\s\S]*?&& !appliedControlSettlementPending/,
|
||
);
|
||
assert.match(
|
||
recoveryEscape,
|
||
/prepareReconfiguration\("select-device", \{\s*appliedRecoveryEscape: true/,
|
||
);
|
||
assert.match(
|
||
prepare,
|
||
/const recoveryEscapeAllowed = Boolean\([\s\S]*?intent === "select-device"[\s\S]*?canPrepareRecoverySelectDevice[\s\S]*?unresolvedAppliedAttempt\?\.connection_mode === "bridge"/,
|
||
);
|
||
assert.match(prepare, /networkRecoveryRequired && !recoveryEscapeAllowed/);
|
||
assert.match(
|
||
prepare,
|
||
/if \(!promotedReconfigurationFence\) return;[\s\S]*?if \(recoveryEscapeAllowed && appliedAttemptRecoveryKey\) \{\s*setEscapedAppliedAttemptKey\(appliedAttemptRecoveryKey\)/,
|
||
);
|
||
assert.doesNotMatch(
|
||
prepare,
|
||
/scanWithResult\(|connect\(|verifyConnection\(|selectFreshDevice\(/,
|
||
);
|
||
assert.match(source, /Начать выбор другого K1/);
|
||
|
||
assert.match(
|
||
source,
|
||
/const canScanAppliedRecovery = Boolean\(\s*unresolvedAppliedAttempt[\s\S]*?connection_mode !== "bridge"[\s\S]*?scanAllowedByPolicy/,
|
||
);
|
||
assert.match(
|
||
search,
|
||
/const recoveryScanAttempt = appliedRecoveryEscape[\s\S]*?canScanAppliedRecovery[\s\S]*?unresolvedAppliedAttempt/,
|
||
);
|
||
assert.match(search, /scanResult = await scanWithResult\(\{ durationSeconds: BLE_DISCOVERY_TIMEOUT_SECONDS \}\)/);
|
||
assert.match(
|
||
search,
|
||
/if \(scanResult\.succeeded && recoveryAttemptKey\) \{\s*setEscapedAppliedAttemptKey\(recoveryAttemptKey\)/,
|
||
);
|
||
assert.equal((search.match(/setEscapedAppliedAttemptKey\(/g) ?? []).length, 1);
|
||
assert.match(
|
||
source,
|
||
/onClick=\{\(\) => void repeatDeviceScan\(\{\s*appliedRecoveryEscape: true/,
|
||
);
|
||
assert.match(source, /Начать новый поиск Bluetooth/);
|
||
assert.match(source, /Подтверждение управляющего подключения/);
|
||
assert.match(source, /BLE-команда не повторяется/);
|
||
assert.match(
|
||
source,
|
||
/status=\{\s*searchActive[\s\S]*?: presentedPhysicalRecoveryRequired[\s\S]*?: presentedConnectionRecoveryRequired[\s\S]*?: networkRecoveryRequired/,
|
||
);
|
||
assert.match(
|
||
source,
|
||
/networkRecoveryRequired\s*&& !connectionAttemptSettling\s*&& !connectionRecoveryVerificationPending \? \(/,
|
||
);
|
||
|
||
assert.match(
|
||
source,
|
||
/if \(!appliedNetworkAttempt\) return;[\s\S]*?setExplicitProvisioningDraft\(null\);[\s\S]*?setSsid\(""\);[\s\S]*?setPassword\(""\)/,
|
||
);
|
||
const appliedAttemptEffect = sourceSlice(
|
||
source,
|
||
"if (!appliedNetworkAttempt) return;",
|
||
"if (connectionAttemptView !== \"retired\") return;",
|
||
);
|
||
assert.doesNotMatch(appliedAttemptEffect, /setConnectionAttemptPresentation\(null\)/);
|
||
assert.doesNotMatch(apply, /void submitConnect|repeatDeviceScan\(/);
|
||
|
||
const settlingState = durableTopologyState();
|
||
settlingState.connection_attempt = {
|
||
schema_version: "missioncore.xgrids-k1-connection-attempt/v1",
|
||
attempt_id: "network-apply-settling",
|
||
connection_mode: "bridge",
|
||
status: "running",
|
||
phase: "network_applied",
|
||
control_state: "unknown",
|
||
stage: "host-route-and-control-endpoint",
|
||
public_error_code: null,
|
||
side_effect_status: "none",
|
||
safe_next_action: "wait-for-current-attempt",
|
||
automatic_retry: false,
|
||
accepted_at: OBSERVED_AT,
|
||
completed_at: null,
|
||
recovery_operation_id: null,
|
||
timeline: [],
|
||
};
|
||
const settlingMarkup = renderProvisioning({
|
||
controller: provisioningController(settlingState),
|
||
desiredMode: "bridge",
|
||
});
|
||
assert.match(settlingMarkup, /Подтверждение управляющего подключения/);
|
||
assert.match(settlingMarkup, /BLE-команда не повторяется/);
|
||
assert.doesNotMatch(settlingMarkup, /Сеть (?:готова|настроена)|Сетевые настройки уже применены/);
|
||
assert.equal(
|
||
(settlingMarkup.match(/class="connection-action-progress"/g) ?? []).length,
|
||
1,
|
||
);
|
||
assert.doesNotMatch(
|
||
settlingMarkup,
|
||
/Проверить подключение без изменения сети|Начать выбор другого K1|Начать новый поиск Bluetooth/,
|
||
);
|
||
});
|
||
|
||
|
||
test("incomplete historical recovery policy stays outside the normal connection form", () => {
|
||
const state = durableTopologyState();
|
||
state.network_mutation_ledger = {
|
||
...state.network_mutation_ledger,
|
||
status: "unresolved",
|
||
mutation_allowed: false,
|
||
operation_id: "operation-002",
|
||
transport_ref: "ble-k1-001",
|
||
intended_mode: "bridge",
|
||
stage: "observing",
|
||
revision: 6,
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: ["observe-configured-device-network"],
|
||
actions: {
|
||
"observe-configured-device-network": {
|
||
allowed: true,
|
||
reason_codes: [],
|
||
target_source: "durable-configured-state",
|
||
required_transport_ref: "ble-k1-001",
|
||
required_connection_mode: null,
|
||
requires_live_gatt_validation: true,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
recommended_action: "observe-configured-device-network",
|
||
};
|
||
|
||
const markup = renderProvisioning({
|
||
controller: provisioningController(state),
|
||
desiredMode: "bridge",
|
||
});
|
||
|
||
assert.doesNotMatch(markup, /Сервер пока не выдал одну точную цель/);
|
||
assert.doesNotMatch(markup, /не подменяют UUID и режим незавершённой операции/);
|
||
assert.doesNotMatch(markup, /Проверить .*без записи/);
|
||
assert.equal(
|
||
buttonMarkupWithText(markup, "Подключиться к сохранённому устройству").length,
|
||
0,
|
||
);
|
||
assert.doesNotMatch(markup, /class="connection-action-progress"/);
|
||
assert.doesNotMatch(markup, /Проверить связь/);
|
||
assert.doesNotMatch(markup, /Подключить K1 к общей сети/);
|
||
});
|
||
|
||
|
||
test("configured endpoint policy explains missing durable evidence without implying BLE", () => {
|
||
const state = durableTopologyState();
|
||
state.semantic_topology_store = {
|
||
status: "empty",
|
||
configured_offline_evidence: false,
|
||
live_connection_authority: false,
|
||
reason_code: null,
|
||
record: null,
|
||
};
|
||
state.connection_policy = {
|
||
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
|
||
facts: { retained_context_is_presence: false },
|
||
allowed_actions: [],
|
||
actions: {
|
||
"inspect-configured-endpoint": {
|
||
allowed: false,
|
||
reason_codes: ["configured-endpoint-unavailable"],
|
||
target_source: "configured-topology",
|
||
required_transport_ref: null,
|
||
requires_live_gatt_validation: false,
|
||
automatic_retry: false,
|
||
},
|
||
},
|
||
recommended_action: "inspect-host-network",
|
||
};
|
||
|
||
const guidance = connectionPolicyOperatorGuidance(
|
||
state,
|
||
"inspect-configured-endpoint",
|
||
);
|
||
assert.match(guidance.reason, /Нет подтверждённого сохранённого адреса K1/);
|
||
assert.match(guidance.nextAction, /Проверьте активную локальную сеть/);
|
||
assert.doesNotMatch(`${guidance.reason} ${guidance.nextAction}`, /Bluetooth|BLE/);
|
||
});
|
||
|
||
test("spatial controls distinguish control authority from data authority", () => {
|
||
const live = runtimeState();
|
||
assert.deepEqual(k1SpatialAuthorityState(live), {
|
||
controlAuthoritative: true,
|
||
dataAuthoritative: true,
|
||
softwareCommanded: true,
|
||
authorityFailure: null,
|
||
});
|
||
|
||
live.connection_supervisor = supervisor({ control: true, data: false });
|
||
const waiting = k1SpatialAuthorityState(live);
|
||
assert.equal(waiting.controlAuthoritative, true);
|
||
assert.equal(waiting.dataAuthoritative, false);
|
||
assert.equal(waiting.softwareCommanded, true);
|
||
assert.match(waiting.authorityFailure, /Телеметрия скрыта/);
|
||
|
||
live.connection_supervisor = supervisor({ control: false, data: false });
|
||
const lost = k1SpatialAuthorityState(live);
|
||
assert.equal(lost.controlAuthoritative, false);
|
||
assert.equal(lost.dataAuthoritative, false);
|
||
assert.equal(lost.softwareCommanded, false);
|
||
assert.match(lost.authorityFailure, /команды устройству запрещены/);
|
||
});
|
||
|
||
test("spatial controls explain the bounded K1 calibration wait before point data", () => {
|
||
const acquisition = runtimeState().acquisition;
|
||
assert.ok(acquisition);
|
||
for (const state of ["awaiting_external_start", "starting"]) {
|
||
const phase = k1SpatialPhasePresentation({ ...acquisition, state }, true);
|
||
assert.equal(phase.busy, true);
|
||
assert.match(phase.label, /калибруется.*облако точек/i);
|
||
assert.match(phase.detail, /десятки секунд/i);
|
||
assert.match(phase.detail, /не перемещайте/i);
|
||
assert.doesNotMatch(phase.detail, /30\s*с/i);
|
||
}
|
||
});
|
||
|
||
test("contour health never promotes selection or replay metrics to live authority", () => {
|
||
const selectedOnly = {
|
||
phase: "starting",
|
||
sourceMode: "idle",
|
||
activeDevice: {
|
||
pluginId: "xgrids-k1",
|
||
modelId: "lixelkity-k1",
|
||
displayName: "XGRIDS LixelKity K1",
|
||
instanceId: "device-k1-001",
|
||
endpointLabel: "192.168.68.52",
|
||
},
|
||
deviceSession: {
|
||
sessionId: "device-session-001",
|
||
deviceId: "device-k1-001",
|
||
compatibilityProfileId: PROFILE_ID,
|
||
connectivity: "unknown",
|
||
},
|
||
metrics: { aiFrameRateHz: 8 },
|
||
};
|
||
const unverified = contourRuntimeAuthorityPresentation(selectedOnly);
|
||
assert.equal(unverified.controlledDevice, null);
|
||
assert.equal(unverified.aiActive, false);
|
||
|
||
const live = contourRuntimeAuthorityPresentation({
|
||
...selectedOnly,
|
||
phase: "streaming",
|
||
sourceMode: "live",
|
||
deviceSession: { ...selectedOnly.deviceSession, connectivity: "connected" },
|
||
});
|
||
assert.equal(live.controlledDevice.instanceId, "device-k1-001");
|
||
assert.equal(live.aiActive, true);
|
||
|
||
const replay = contourRuntimeAuthorityPresentation({
|
||
...selectedOnly,
|
||
phase: "replaying",
|
||
sourceMode: "replay",
|
||
deviceSession: { ...selectedOnly.deviceSession, connectivity: "connected" },
|
||
});
|
||
assert.equal(replay.aiActive, false);
|
||
});
|
||
|
||
|
||
test("the recovery panel explains the reviewed K1 network-not-found reply", () => {
|
||
const state = terminalConnectionRecoveryState();
|
||
state.connection_attempt.public_error_code = "k1-wifi-network-not-found";
|
||
const markup = renderToStaticMarkup(createElement(K1OperatorError, {
|
||
attempt: state.connection_attempt, onRefresh() {}, onClear() {},
|
||
}));
|
||
assert.match(markup, /<strong>Не удалось подключить K1 к Wi‑Fi<\/strong>/);
|
||
assert.match(markup, /указанная сеть не найдена/);
|
||
assert.match(markup, /Проверьте название сети и пароль/);
|
||
assert.doesNotMatch(markup, /ошибкой Bluetooth|INVALID_PDU/);
|
||
});
|
||
|
||
test("a reviewed Wi-Fi rejection offers explicit network setup for the same K1", () => {
|
||
const state = terminalConnectionRecoveryState();
|
||
state.connection_attempt.public_error_code = "k1-wifi-network-not-found";
|
||
const controller = {
|
||
...provisioningController(state), error: "private transport error",
|
||
errorCorrelation: {
|
||
action: "connect", runtimeId: state.snapshot_runtime_id, leaseGeneration: 0,
|
||
connectionAttemptId: state.connection_attempt.attempt_id,
|
||
},
|
||
getConnectionRecoveryObservationTarget: () => recommendedConnectionRecoveryObservationTarget(state),
|
||
};
|
||
const markup = renderProvisioningWithAttempt({ controller, desiredMode: "bridge" },
|
||
provisioningAttemptPresentation({ snapshotRuntimeId: state.snapshot_runtime_id,
|
||
attemptId: state.connection_attempt.attempt_id, localPhase: "failed" }), true);
|
||
assert.equal(buttonMarkupWithText(markup, "Указать сеть Wi‑Fi заново").length, 1);
|
||
assert.doesNotMatch(markup, /Подключить новый K1|private transport error/);
|
||
});
|
||
|
||
test("a failed Connect after completed Bluetooth search exposes recovery instead of a locked form", () => {
|
||
const state = terminalConnectionRecoveryState();
|
||
state.ble_discovery_generation = 6;
|
||
state.connection_attempt.public_error_code = "BleakGATTProtocolError";
|
||
state.connection_attempt.stage = "gatt-write-failed";
|
||
const controller = {
|
||
...provisioningController(state),
|
||
error: "private transport exception must stay hidden",
|
||
errorCorrelation: {
|
||
action: "connect", runtimeId: state.snapshot_runtime_id, leaseGeneration: 0,
|
||
connectionAttemptId: state.connection_attempt.attempt_id,
|
||
},
|
||
getConnectionRecoveryObservationTarget: () => recommendedConnectionRecoveryObservationTarget(state),
|
||
};
|
||
const markup = renderProvisioningWithAttempt({ controller, desiredMode: "bridge" },
|
||
provisioningAttemptPresentation({ snapshotRuntimeId: state.snapshot_runtime_id,
|
||
attemptId: state.connection_attempt.attempt_id, localPhase: "failed" }), true);
|
||
assert.equal(buttonMarkupWithText(markup, "Переподключиться").length, 1);
|
||
assert.equal(buttonMarkupWithText(markup, "Подключить новый K1").length, 1);
|
||
assert.match(markup, /K1 ответил ошибкой Bluetooth/);
|
||
assert.doesNotMatch(markup, /Подключение не выполнено|дождитесь, пока система|private transport exception|Пароль передан/);
|
||
});
|