chore(k1): checkpoint connection recovery work

This commit is contained in:
DCCONSTRUCTIONS
2026-08-21 08:12:22 +03:00
parent a3138f3d71
commit c7843a3c7e
22 changed files with 969 additions and 293 deletions
@@ -65,6 +65,7 @@ let shellPresentation;
let reconfigurationAllowsFreshDevice;
let readOnlyObservationShowsNetworkUnavailable;
let readOnlyFailureShowsNetworkUnavailable;
let savedBridgeRequiresNetworkSetup;
let provisioningFailureRequiresFreshCandidate;
let trustedConnectionBinding;
let transportRefEquivalenceKey;
@@ -183,6 +184,7 @@ before(async () => {
reconfigurationAllowsFreshDevice,
readOnlyObservationShowsNetworkUnavailable,
readOnlyFailureShowsNetworkUnavailable,
savedBridgeRequiresNetworkSetup,
provisioningFailureRequiresFreshCandidate,
trustedConnectionBinding,
transportRefEquivalenceKey,
@@ -1057,12 +1059,15 @@ function connectionPipelinesController(state) {
};
}
function renderConnectionPipelines(state) {
function renderConnectionPipelines(state, pendingAction = null) {
return renderToStaticMarkup(createElement(K1ConnectionPipelines, {
controller: connectionPipelinesController(state),
controller: {
...connectionPipelinesController(state),
pendingAction,
},
desiredConnectionMode: "bridge",
onDesiredConnectionModeChange() {},
operationalPanelsVisible: shouldRenderK1OperationalPanels(state),
operationalPanelsVisible: shouldRenderK1OperationalPanels(state, pendingAction),
openSpatialScene() {},
activateAutomaticSpatialSource() {},
sourceLabel: "Ожидание",
@@ -2872,7 +2877,7 @@ test("runtime errors borrow diagnostics only from their exact Connect attempt",
);
});
test("connection recovery observation follows recommended exact policy then safe priority", () => {
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 = [{
@@ -2893,7 +2898,7 @@ test("connection recovery observation follows recommended exact policy then safe
state.connection_policy = {
schema_version: "missioncore.xgrids-k1-connection-policy/v1",
facts: { retained_context_is_presence: false },
recommended_action: "observe-configured-device-network",
recommended_action: "observe-fresh-device-network",
allowed_actions: [
"observe-current-device-network",
"observe-configured-device-network",
@@ -2913,10 +2918,21 @@ test("connection recovery observation follows recommended exact policy then safe
};
const recommended = recommendedConnectionRecoveryObservationTarget(state);
assert.equal(recommended?.action, "observe-configured-device-network");
assert.equal(recommended?.deviceId, "configured-k1");
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");
@@ -3109,6 +3125,19 @@ test("cold disconnected connection SSR hides every operational panel", () => {
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";
@@ -3911,6 +3940,34 @@ test("explicit read-only recovery keeps its network-unavailable classification",
}
});
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",
@@ -4256,7 +4313,7 @@ test("fresh Scan keeps retired audit rows selectable without recovery I/O", () =
assertCanonicalConnectionCopy(historicalAuditMarkup);
});
test("only current network recovery safe-next states retain a historical applied attempt", () => {
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",
@@ -4283,7 +4340,7 @@ test("only current network recovery safe-next states retain a historical applied
...attempt,
safe_next_action: safeNextAction,
}),
true,
false,
safeNextAction,
);
}
@@ -5205,6 +5262,51 @@ test("an unresolved physical command has one explicit server-bound read-only rec
/Результаты последнего Bluetooth-поиска|nearby-unrelated-device|>Выбрать<|>Применить</,
);
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"];
@@ -5258,9 +5360,10 @@ test("an unresolved physical command has one explicit server-bound read-only rec
/scanWithResult\(|connect\(|retireUnavailable|reopenRetired|reconcilePhysicalCommand/,
);
assert.match(physicalRecovery, /physicalRecoveryTarget\?\.serverBound/);
assert.match(physicalRecovery, /surfaceErrors: false/);
});
test("supervisor DeviceInfo Verify keeps the exact physical reconnect visible", () => {
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";
@@ -5299,11 +5402,20 @@ test("supervisor DeviceInfo Verify keeps the exact physical reconnect visible",
recommended_action: "scan-ble",
allowed_actions: [
"scan-ble",
"observe-fresh-device-network",
"inspect-configured-endpoint",
"verify-control-device-info",
],
actions: {
"observe-fresh-device-network": deniedObservation("fresh-scan"),
"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",
),
@@ -5320,6 +5432,13 @@ test("supervisor DeviceInfo Verify keeps the exact physical reconnect visible",
},
},
};
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",
@@ -5341,6 +5460,7 @@ test("supervisor DeviceInfo Verify keeps the exact physical reconnect visible",
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);
});
@@ -5929,8 +6049,16 @@ test("terminal Apply recovery is bounded and never claims that the network is re
controller: provisioningController(state),
desiredMode: "bridge",
}, presentation);
assert.match(markup, /Управление не подтверждено/);
assert.match(markup, /Попытка настройки завершена/);
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"/);
@@ -6231,7 +6359,7 @@ test("unknown and cold durable recovery offer one reconnect and one new-device p
desiredMode: "bridge",
}, presentation);
assert.doesNotMatch(unrelatedMarkup, /op-b7ea404d/);
assert.match(unrelatedMarkup, /Результат применения сети не подтверждён/);
assert.match(unrelatedMarkup, /Сохранённое подключение/);
});
test("read-only reconnect keeps its card and loader across a transient projection", async () => {
@@ -6294,7 +6422,7 @@ test("read-only reconnect keeps its card and loader across a transient projectio
assert.match(pendingReconnect[0], /disabled/);
assert.match(pendingReconnect[0], /aria-busy="true"/);
assert.match(pendingReconnect[0], /nodedc-activity-indicator/);
assert.match(pendingMarkup, /Сохранённое подключение требует проверки/);
assert.match(pendingMarkup, /Сохранённое подключение/);
assert.doesNotMatch(pendingMarkup, /connection-action-progress/);
assert.equal(
harness.stateValue(emptyReadOnlyReconnectPresentation)?.kind,
@@ -6320,7 +6448,7 @@ test("read-only reconnect keeps its card and loader across a transient projectio
}
});
test("cold saved new-device path resets once, performs zero Scan, and survives reload", async () => {
test("cold saved target keeps fast reconnect after a reset and reload", async () => {
const state = terminalConnectionRecoveryState();
state.connection_attempt = null;
const target = recommendedConnectionRecoveryObservationTarget(state);
@@ -6424,14 +6552,18 @@ test("cold saved new-device path resets once, performs zero Scan, and survives r
},
desiredMode: "bridge",
});
assert.equal(buttonMarkupWithText(reloadedMarkup, "Переподключиться").length, 0);
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.doesNotMatch(
reloadedMarkup,
/Нужна проверка|Прежнее подключение не подтверждено|Проверяем прежний K1/,
);
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", () => {
@@ -6478,7 +6610,7 @@ test("a current post-reset failed or unknown attempt outranks the durable reset
},
desiredMode: "bridge",
});
assert.match(unknownReloadMarkup, /Результат применения сети не подтверждён/);
assert.match(unknownReloadMarkup, /Сохранённое подключение/);
assert.equal(
buttonMarkupWithText(unknownReloadMarkup, "Переподключиться").length,
1,
@@ -6501,9 +6633,20 @@ test("a current post-reset failed or unknown attempt outranks the durable reset
controller: provisioningController(failed),
desiredMode: "bridge",
});
assert.match(failedReloadMarkup, /Управление не подтверждено/);
assert.match(failedReloadMarkup, /Новый выбор временно заблокирован/);
assert.doesNotMatch(failedReloadMarkup, /Совпадений нет|Ожидает/);
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", () => {