chore(k1): checkpoint connection recovery work
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user