Корректное ожидание подтверждения Bridge

This commit is contained in:
DCCONSTRUCTIONS
2026-08-23 12:14:56 +03:00
parent 7d911b4b1f
commit 456141a3f3
4 changed files with 133 additions and 12 deletions
@@ -67,6 +67,7 @@ let reconfigurationAllowsFreshDevice;
let readOnlyObservationShowsNetworkUnavailable;
let readOnlyFailureShowsNetworkUnavailable;
let savedBridgeRequiresNetworkSetup;
let isConnectionControlBootstrapSettling;
let provisioningFailureRequiresFreshCandidate;
let trustedConnectionBinding;
let transportRefEquivalenceKey;
@@ -189,6 +190,7 @@ before(async () => {
readOnlyObservationShowsNetworkUnavailable,
readOnlyFailureShowsNetworkUnavailable,
savedBridgeRequiresNetworkSetup,
isConnectionControlBootstrapSettling,
provisioningFailureRequiresFreshCandidate,
trustedConnectionBinding,
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", () => {
const source = readFileSync(provisioningSourceUrl, "utf8");
const presentationType = sourceSlice(
@@ -18,6 +18,7 @@ import {
backendConnectionTopology,
connectionAttemptForRuntimeError,
hasControlAuthority,
isConnectionControlBootstrapSettling,
isConfirmedLiveState,
isPhysicalStopRecoverySettling,
isRecoveredPhysicalScanning,
@@ -274,12 +275,16 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps)
: sourceRuntimeBusy || preparedAcquisition
? "warning"
: "neutral";
const connectionControlBootstrapSettling =
isConnectionControlBootstrapSettling(state);
const connectionPhaseLabel = livePreparationPending
? "Подготовка приёма"
: sourceRuntimeBusy || preparedAcquisition
? sourceLabel
: activeRecoveryPresentation
? activeRecoveryPresentation.title
: connectionControlBootstrapSettling
? "Подтверждение подключения"
: physicalStopRecoverySettling
? "Завершение остановки"
: recoveredPhysicalScanning
@@ -307,6 +312,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps)
? sourceTone
: activeRecoveryPresentation
? activeRecoveryPresentation.tone
: connectionControlBootstrapSettling
? "accent"
: physicalRecoveryRequired
? "warning"
: projectedPhase === "error"
@@ -322,6 +329,8 @@ export function XgridsK1Connection({ model, host }: DevicePluginConnectionProps)
? "Команда остановки уже принята. Завершение выполняется без повторной команды."
: activeRecoveryPresentation
? activeRecoveryPresentation.detail
: connectionControlBootstrapSettling
? "Сеть применена. Сервис проверяет управляющее подключение без повторения BLE-команды."
: recoveredPhysicalScanning
? "Локальная запись остановлена, но сканирование ещё продолжается."
: physicalRecoveryRequired
@@ -2021,16 +2021,18 @@ export function K1ProvisioningPipeline({
const changeNetworkActionApplicable = connectionMode === "bridge"
&& connectedReconfigurationActionApplicable(state, "prepare-change-network");
const showNetworkStep = Boolean(
!presentedPhysicalRecoveryRequired
&& !presentedConnectionRecoveryRequired
&& (
connectionPresentationEstablished
|| explicitProvisioningDraftRetained
|| explicitProvisioningDraftContextRetained
|| unresolvedAppliedAttempt
|| connectionAttemptSettling
|| connectionAttemptFailed
|| (changeNetworkDialogue && Boolean(changeNetworkRequiredDeviceId))
connectionAttemptSettling
|| (
!presentedPhysicalRecoveryRequired
&& !presentedConnectionRecoveryRequired
&& (
connectionPresentationEstablished
|| explicitProvisioningDraftRetained
|| explicitProvisioningDraftContextRetained
|| unresolvedAppliedAttempt
|| connectionAttemptFailed
|| (changeNetworkDialogue && Boolean(changeNetworkRequiredDeviceId))
)
)
);
const requestExplicitProvisioning = useCallback((
@@ -3296,6 +3298,8 @@ export function K1ProvisioningPipeline({
? "accent"
: connectionEstablished
? "success"
: connectionAttemptSettling
? "accent"
: networkRecoveryRequired
|| presentedPhysicalRecoveryRequired
|| explicitProvisioningDraftStale
@@ -3330,7 +3334,7 @@ export function K1ProvisioningPipeline({
?? "Сервис не определил точный прежний K1 для безопасной проверки"}
</small>
</div>
) : presentedPhysicalRecoveryRequired ? (
) : presentedPhysicalRecoveryRequired && !connectionAttemptSettling ? (
<div
className="field-stack"
aria-busy={physicalReconnectPending || undefined}
@@ -3388,7 +3392,7 @@ export function K1ProvisioningPipeline({
</p>
) : null}
</div>
) : presentedConnectionRecoveryRequired ? (
) : presentedConnectionRecoveryRequired && !connectionAttemptSettling ? (
<div
className="field-stack"
aria-busy={connectionReconnectPending || undefined}
@@ -466,6 +466,26 @@ export function sourceStatusLabel(state: XgridsK1State | null | undefined): stri
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(
state: XgridsK1State | null | undefined,
action: string,