Unify K1 discovery ownership and verification across enrollment paths

This commit is contained in:
DCCONSTRUCTIONS
2026-09-07 20:04:46 +03:00
parent 9c4d70b0e9
commit 762d77ef95
27 changed files with 615 additions and 191 deletions
@@ -260,7 +260,8 @@ test("K1 connection surface enforces one scan, local selection, and one Apply",
assert.doesNotMatch(provisioning, /allow_host_wifi_switch/);
assert.doesNotMatch(provisioning, /(?:color|background(?:-color)?):\s*(?:#[0-9a-f]{3,8}|rgba?\()/i);
for (const sharedControl of ["Button", "IconButton", "TextField", "ActivityIndicator", "StatusBadge"]) {
assert.match(provisioning, new RegExp(`<${sharedControl}\\b`), sharedControl);
const password = readFileSync(join(pluginFrontendRoot, "components/K1WifiPasswordField.tsx"), "utf8");
assert.match(provisioning + password, new RegExp(`<${sharedControl}\\b`), sharedControl);
}
assert.doesNotMatch(
provisioning,
@@ -736,7 +737,7 @@ test("K1 provisioning keeps the operator draft separate from the backend lease",
assert.match(provisioning, /scanWithResult\(\{ durationSeconds: BLE_DISCOVERY_TIMEOUT_SECONDS \}\)/);
assert.match(
provisioning,
/onChange=\{\(event\) => setPassword\(event\.target\.value\)\}/,
/<K1WifiPasswordField[\s\S]*?onChange=\{setPassword\}/,
);
});
@@ -16,6 +16,7 @@ let K1ConnectionPipelines;
let physicalRecoveryConnectionDetail;
let shouldRenderK1OperationalPanels;
let K1ProvisioningPipeline;
let K1WifiPasswordField;
let RuntimeActionFenceTestContext;
let LOCAL_OPERATION_PROGRESS_DELAY_MILLISECONDS;
let emptySearchPresentation;
@@ -102,6 +103,9 @@ before(async () => {
logLevel: "silent",
server: { middlewareMode: true },
});
({K1WifiPasswordField} = await server.ssrLoadModule(
"@xgrids-k1/frontend/components/K1WifiPasswordField.tsx",
));
({ normalizeXgridsK1MissionState } = await server.ssrLoadModule(
"@xgrids-k1/frontend/runtimeContext.tsx",
));
@@ -277,7 +281,7 @@ test("K1 one-intent source contract makes mode reset explicit and keeps device I
assert.match(resultRows, /onSelect=\{\(\) => selectCandidate\(device\)\}/);
assert.doesNotMatch(resultRows, /Переподключиться|reopen|verifyConnection/);
assert.match(source, /onChange=\{\(event\) => setSsid\(event\.target\.value\)\}/);
assert.match(source, /onChange=\{\(event\) => setPassword\(event\.target\.value\)\}/);
assert.match(source, /<K1WifiPasswordField[\s\S]*?onChange=\{setPassword\}/);
assert.equal((apply.match(/await connect\(/g) ?? []).length, 1);
assert.doesNotMatch(
@@ -290,9 +294,13 @@ test("K1 one-intent source contract makes mode reset explicit and keeps device I
source,
/(?:color|background(?:-color)?):\s*(?:#[0-9a-f]{3,8}|rgba?\()/i,
);
const passwordSource = readFileSync(new URL(
"../../../plugins/xgrids-k1/frontend/src/components/K1WifiPasswordField.tsx", import.meta.url,
), "utf8");
for (const sharedControl of ["Button", "IconButton", "TextField", "ActivityIndicator", "StatusBadge"]) {
assert.match(source, new RegExp(`<${sharedControl}\\b`), sharedControl);
assert.match(source + passwordSource, new RegExp(`<${sharedControl}\\b`), sharedControl);
}
assert.doesNotMatch(passwordSource, /<(?:button|input)\b/);
assert.equal((source.match(/buttonLabel:\s*"Применить"/g) ?? []).length, 3);
assert.match(source, /устарел|stale/i);
assert.match(source, /network_outcome_unknown|safe_to_retry/i);
@@ -1181,7 +1189,7 @@ function elementByProp(node, propName, expectedValue) {
return elementByProp(node.props.children, propName, expectedValue);
}
function createStatefulProvisioningHarness(initialProps) {
function createStatefulProvisioningHarness(initialProps, Component = K1ProvisioningPipeline) {
const hookSlots = [];
let currentProps = initialProps;
let capturedTree = null;
@@ -1296,7 +1304,7 @@ function createStatefulProvisioningHarness(initialProps) {
};
try {
capturedTree = K1ProvisioningPipeline(currentProps);
capturedTree = Component(currentProps);
} finally {
dispatcher.useState = originals.useState;
dispatcher.useRef = originals.useRef;
@@ -4669,23 +4677,11 @@ test("post-reset Scan selects the exact prior UUID locally and opens network fie
"local selection must not manufacture a backend Apply attempt",
);
const passwordField = elementByProp(tree, "label", "Пароль WiFi");
const passwordField = elementByProp(tree, "placeholder", "Введите пароль");
assert.ok(passwordField);
assert.equal(passwordField.props.type, "password");
passwordField.props.onChange({ target: { value: "test-only-password" } });
passwordField.props.onChange("test-only-password");
tree = harness.render(props());
harness.flushEffects();
const showPassword = elementByProp(tree, "label", "Показать пароль");
assert.ok(showPassword);
assert.equal(showPassword.props.disabled, false);
showPassword.props.onClick();
tree = harness.render(props());
harness.flushEffects();
assert.equal(
elementByProp(tree, "label", "Пароль WiFi").props.type,
"text",
);
assert.ok(elementByProp(tree, "label", "Скрыть пароль"));
const chooseAnother = actionByLabel(tree, "Выбрать другое");
assert.ok(chooseAnother);
@@ -7333,7 +7329,7 @@ test("Step-2 credentials and Apply share one canonical field stack", () => {
assert.match(
form,
/:\s*\(\s*<div className="field-stack"[\s\S]*?label="Пароль WiFi"[\s\S]*?\{applyAction\}\s*<\/div>/,
/:\s*\(\s*<div className="field-stack"[\s\S]*?<K1WifiPasswordField[\s\S]*?\{applyAction\}\s*<\/div>/,
);
assert.match(form, /disabled=\{provisioningFieldsDisabled\}/);
assert.match(form, /value=\{connectionAttemptOwnsDraft \? "" : password\}/);
@@ -7769,3 +7765,26 @@ test("a failed Connect after completed Bluetooth search exposes recovery instead
assert.match(markup, /K1 ответил ошибкой Bluetooth/);
assert.doesNotMatch(markup, /Подключение не выполнено|дождитесь, пока система|private transport exception|Пароль передан/);
});
test("shared K1 password field reveals on demand and hides after clearing", () => {
let value = "";
const props = () => ({value, onChange:next=>{value=next;}, disabled:false});
const harness = createStatefulProvisioningHarness(props(), K1WifiPasswordField);
try {
let tree = harness.render(props());
assert.equal(elementByProp(tree, "label", "Показать пароль").props.disabled, true);
elementByProp(tree, "label", "Пароль WiFi").props.onChange({target:{value:"test-only-value"}});
tree = harness.render(props()); harness.flushEffects();
elementByProp(tree, "label", "Показать пароль").props.onClick();
tree = harness.render(props());
assert.equal(elementByProp(tree, "label", "Пароль WiFi").props.type, "text");
assert.equal(elementByProp(tree, "label", "Скрыть пароль").props['aria-pressed'], true);
value = "";
tree = harness.render(props()); harness.flushEffects();
tree = harness.render(props());
assert.equal(elementByProp(tree, "label", "Пароль WiFi").props.type, "password");
assert.equal(elementByProp(tree, "label", "Показать пароль").props.disabled, true);
assert.equal(elementByProp(tree, "label", "Пароль WiFi").props.autoComplete, "off");
} finally { harness.dispose(); }
});
@@ -204,3 +204,31 @@ test('pending enrollment is presented by its action button without a second noti
}}),'');
}
});
test('discovery errors preserve their cause and never imply a Wi-Fi refusal',()=>{
for(const [code,text,requiresScan] of [
['ble-discovery-busy',/поиск уже выполняется/i,false],
['ble-adapter-unavailable',/Bluetooth недоступен/,false],
['ble-discovery-failed',/Служба Bluetooth/,false],
['ble-selected-device-unavailable',/Выбранный K1 не найден/,true],
]){
const state={...initial,connection_attempt:{schema_version:'missioncore.xgrids-k1-connection-attempt/v1',
attempt_id:'test',status:'failed',stage:'resolution-failed',phase:'network_not_applied',
public_error_code:code,side_effect_status:'none'}};
assert.match(api.enrollmentNotice(state),text);
assert.match(api.enrollmentNotice(state),/Настройки Wi-Fi не были отправлены/);
assert.doesNotMatch(api.enrollmentNotice(state),/Проверьте.*пароль/);
assert.equal(api.enrollmentBluetoothFailure(state),requiresScan);
state.connection_attempt.side_effect_status='unknown';
state.connection_attempt.phase='network_outcome_unknown';
assert.doesNotMatch(api.enrollmentNotice(state),/не были отправлены/);
}
});
test('legacy resolution failure invalidates the form before a second stale submission',()=>{
const state={...initial,connection_attempt:{schema_version:'missioncore.xgrids-k1-connection-attempt/v1',
attempt_id:'test',status:'failed',stage:'resolution-failed',public_error_code:'BleakDBusError',side_effect_status:'none'}};
assert.equal(api.enrollmentBluetoothFailure(state),true);
assert.match(api.enrollmentNotice(state),/Найдите K1 ещё раз/);
});