|
|
|
@@ -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", () => {
|
|
|
|
|