Корректное ожидание подтверждения Bridge
This commit is contained in:
@@ -67,6 +67,7 @@ let reconfigurationAllowsFreshDevice;
|
|||||||
let readOnlyObservationShowsNetworkUnavailable;
|
let readOnlyObservationShowsNetworkUnavailable;
|
||||||
let readOnlyFailureShowsNetworkUnavailable;
|
let readOnlyFailureShowsNetworkUnavailable;
|
||||||
let savedBridgeRequiresNetworkSetup;
|
let savedBridgeRequiresNetworkSetup;
|
||||||
|
let isConnectionControlBootstrapSettling;
|
||||||
let provisioningFailureRequiresFreshCandidate;
|
let provisioningFailureRequiresFreshCandidate;
|
||||||
let trustedConnectionBinding;
|
let trustedConnectionBinding;
|
||||||
let transportRefEquivalenceKey;
|
let transportRefEquivalenceKey;
|
||||||
@@ -189,6 +190,7 @@ before(async () => {
|
|||||||
readOnlyObservationShowsNetworkUnavailable,
|
readOnlyObservationShowsNetworkUnavailable,
|
||||||
readOnlyFailureShowsNetworkUnavailable,
|
readOnlyFailureShowsNetworkUnavailable,
|
||||||
savedBridgeRequiresNetworkSetup,
|
savedBridgeRequiresNetworkSetup,
|
||||||
|
isConnectionControlBootstrapSettling,
|
||||||
provisioningFailureRequiresFreshCandidate,
|
provisioningFailureRequiresFreshCandidate,
|
||||||
trustedConnectionBinding,
|
trustedConnectionBinding,
|
||||||
transportRefEquivalenceKey,
|
transportRefEquivalenceKey,
|
||||||
@@ -6228,6 +6230,92 @@ test("Apply presentation stays correlated through ACK, bootstrap, terminal and c
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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", () => {
|
test("Apply spends secret state before I/O and never stores a password in its presentation latch", () => {
|
||||||
const source = readFileSync(provisioningSourceUrl, "utf8");
|
const source = readFileSync(provisioningSourceUrl, "utf8");
|
||||||
const presentationType = sourceSlice(
|
const presentationType = sourceSlice(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
backendConnectionTopology,
|
backendConnectionTopology,
|
||||||
connectionAttemptForRuntimeError,
|
connectionAttemptForRuntimeError,
|
||||||
hasControlAuthority,
|
hasControlAuthority,
|
||||||
|
isConnectionControlBootstrapSettling,
|
||||||
isConfirmedLiveState,
|
isConfirmedLiveState,
|
||||||
isPhysicalStopRecoverySettling,
|
isPhysicalStopRecoverySettling,
|
||||||
isRecoveredPhysicalScanning,
|
isRecoveredPhysicalScanning,
|
||||||
@@ -274,12 +275,16 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps)
|
|||||||
: sourceRuntimeBusy || preparedAcquisition
|
: sourceRuntimeBusy || preparedAcquisition
|
||||||
? "warning"
|
? "warning"
|
||||||
: "neutral";
|
: "neutral";
|
||||||
|
const connectionControlBootstrapSettling =
|
||||||
|
isConnectionControlBootstrapSettling(state);
|
||||||
const connectionPhaseLabel = livePreparationPending
|
const connectionPhaseLabel = livePreparationPending
|
||||||
? "Подготовка приёма"
|
? "Подготовка приёма"
|
||||||
: sourceRuntimeBusy || preparedAcquisition
|
: sourceRuntimeBusy || preparedAcquisition
|
||||||
? sourceLabel
|
? sourceLabel
|
||||||
: activeRecoveryPresentation
|
: activeRecoveryPresentation
|
||||||
? activeRecoveryPresentation.title
|
? activeRecoveryPresentation.title
|
||||||
|
: connectionControlBootstrapSettling
|
||||||
|
? "Подтверждение подключения"
|
||||||
: physicalStopRecoverySettling
|
: physicalStopRecoverySettling
|
||||||
? "Завершение остановки"
|
? "Завершение остановки"
|
||||||
: recoveredPhysicalScanning
|
: recoveredPhysicalScanning
|
||||||
@@ -307,6 +312,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps)
|
|||||||
? sourceTone
|
? sourceTone
|
||||||
: activeRecoveryPresentation
|
: activeRecoveryPresentation
|
||||||
? activeRecoveryPresentation.tone
|
? activeRecoveryPresentation.tone
|
||||||
|
: connectionControlBootstrapSettling
|
||||||
|
? "accent"
|
||||||
: physicalRecoveryRequired
|
: physicalRecoveryRequired
|
||||||
? "warning"
|
? "warning"
|
||||||
: projectedPhase === "error"
|
: projectedPhase === "error"
|
||||||
@@ -322,6 +329,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps)
|
|||||||
? "Команда остановки уже принята. Завершение выполняется без повторной команды."
|
? "Команда остановки уже принята. Завершение выполняется без повторной команды."
|
||||||
: activeRecoveryPresentation
|
: activeRecoveryPresentation
|
||||||
? activeRecoveryPresentation.detail
|
? activeRecoveryPresentation.detail
|
||||||
|
: connectionControlBootstrapSettling
|
||||||
|
? "Сеть применена. Сервис проверяет управляющее подключение без повторения BLE-команды."
|
||||||
: recoveredPhysicalScanning
|
: recoveredPhysicalScanning
|
||||||
? "Локальная запись остановлена, но сканирование ещё продолжается."
|
? "Локальная запись остановлена, но сканирование ещё продолжается."
|
||||||
: physicalRecoveryRequired
|
: physicalRecoveryRequired
|
||||||
|
|||||||
@@ -2021,6 +2021,8 @@ export function K1ProvisioningPipeline({
|
|||||||
const changeNetworkActionApplicable = connectionMode === "bridge"
|
const changeNetworkActionApplicable = connectionMode === "bridge"
|
||||||
&& connectedReconfigurationActionApplicable(state, "prepare-change-network");
|
&& connectedReconfigurationActionApplicable(state, "prepare-change-network");
|
||||||
const showNetworkStep = Boolean(
|
const showNetworkStep = Boolean(
|
||||||
|
connectionAttemptSettling
|
||||||
|
|| (
|
||||||
!presentedPhysicalRecoveryRequired
|
!presentedPhysicalRecoveryRequired
|
||||||
&& !presentedConnectionRecoveryRequired
|
&& !presentedConnectionRecoveryRequired
|
||||||
&& (
|
&& (
|
||||||
@@ -2028,10 +2030,10 @@ export function K1ProvisioningPipeline({
|
|||||||
|| explicitProvisioningDraftRetained
|
|| explicitProvisioningDraftRetained
|
||||||
|| explicitProvisioningDraftContextRetained
|
|| explicitProvisioningDraftContextRetained
|
||||||
|| unresolvedAppliedAttempt
|
|| unresolvedAppliedAttempt
|
||||||
|| connectionAttemptSettling
|
|
||||||
|| connectionAttemptFailed
|
|| connectionAttemptFailed
|
||||||
|| (changeNetworkDialogue && Boolean(changeNetworkRequiredDeviceId))
|
|| (changeNetworkDialogue && Boolean(changeNetworkRequiredDeviceId))
|
||||||
)
|
)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const requestExplicitProvisioning = useCallback((
|
const requestExplicitProvisioning = useCallback((
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
@@ -3296,6 +3298,8 @@ export function K1ProvisioningPipeline({
|
|||||||
? "accent"
|
? "accent"
|
||||||
: connectionEstablished
|
: connectionEstablished
|
||||||
? "success"
|
? "success"
|
||||||
|
: connectionAttemptSettling
|
||||||
|
? "accent"
|
||||||
: networkRecoveryRequired
|
: networkRecoveryRequired
|
||||||
|| presentedPhysicalRecoveryRequired
|
|| presentedPhysicalRecoveryRequired
|
||||||
|| explicitProvisioningDraftStale
|
|| explicitProvisioningDraftStale
|
||||||
@@ -3330,7 +3334,7 @@ export function K1ProvisioningPipeline({
|
|||||||
?? "Сервис не определил точный прежний K1 для безопасной проверки"}
|
?? "Сервис не определил точный прежний K1 для безопасной проверки"}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
) : presentedPhysicalRecoveryRequired ? (
|
) : presentedPhysicalRecoveryRequired && !connectionAttemptSettling ? (
|
||||||
<div
|
<div
|
||||||
className="field-stack"
|
className="field-stack"
|
||||||
aria-busy={physicalReconnectPending || undefined}
|
aria-busy={physicalReconnectPending || undefined}
|
||||||
@@ -3388,7 +3392,7 @@ export function K1ProvisioningPipeline({
|
|||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : presentedConnectionRecoveryRequired ? (
|
) : presentedConnectionRecoveryRequired && !connectionAttemptSettling ? (
|
||||||
<div
|
<div
|
||||||
className="field-stack"
|
className="field-stack"
|
||||||
aria-busy={connectionReconnectPending || undefined}
|
aria-busy={connectionReconnectPending || undefined}
|
||||||
|
|||||||
@@ -466,6 +466,26 @@ export function sourceStatusLabel(state: XgridsK1State | null | undefined): stri
|
|||||||
return "Ожидание";
|
return "Ожидание";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The network write has already crossed its single mutation boundary and the
|
||||||
|
* service is now proving control authority without another BLE command. This
|
||||||
|
* current connection intent must remain visibly pending even when an older
|
||||||
|
* physical-command checkpoint is being reconciled by the same bootstrap.
|
||||||
|
*/
|
||||||
|
export function isConnectionControlBootstrapSettling(
|
||||||
|
state: XgridsK1State | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
const attempt = state?.connection_attempt;
|
||||||
|
if (
|
||||||
|
!attempt
|
||||||
|
|| attempt.phase !== "network_applied"
|
||||||
|
|| attempt.control_state !== "unknown"
|
||||||
|
) return false;
|
||||||
|
if (["accepted", "running"].includes(attempt.status)) return true;
|
||||||
|
return attempt.status === "succeeded"
|
||||||
|
&& attempt.safe_next_action === "verify-control-read-only";
|
||||||
|
}
|
||||||
|
|
||||||
export function operationByIdempotencyKey(
|
export function operationByIdempotencyKey(
|
||||||
state: XgridsK1State | null | undefined,
|
state: XgridsK1State | null | undefined,
|
||||||
action: string,
|
action: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user