fix(k1): restore canonical local connection lifecycle
This commit is contained in:
@@ -635,6 +635,72 @@ test("a provisioned address is not presented as a verified device connection", (
|
||||
}), "connected");
|
||||
});
|
||||
|
||||
test("K1 provisioning mutations require the operator's fresh BLE candidate", () => {
|
||||
const backendLease = {
|
||||
selected_device_id: "stale-backend-lease-device",
|
||||
connection_mode: "bridge",
|
||||
devices: [
|
||||
{
|
||||
device_id: "fresh-scan-candidate",
|
||||
name: "K1 candidate",
|
||||
connectable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert.equal(
|
||||
lifecycle.provisioningCandidateById(backendLease.devices, ""),
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
lifecycle.provisioningCandidateById(
|
||||
backendLease.devices,
|
||||
backendLease.selected_device_id,
|
||||
),
|
||||
null,
|
||||
);
|
||||
assert.equal(lifecycle.canSubmitProvisioningMutation({
|
||||
devices: backendLease.devices,
|
||||
selectedDeviceId: "expired-scan-candidate",
|
||||
powerConfirmed: true,
|
||||
credentialsReady: true,
|
||||
isBusy: false,
|
||||
}), false);
|
||||
assert.equal(lifecycle.canSubmitProvisioningMutation({
|
||||
devices: backendLease.devices,
|
||||
selectedDeviceId: "fresh-scan-candidate",
|
||||
powerConfirmed: true,
|
||||
credentialsReady: true,
|
||||
isBusy: false,
|
||||
}), true);
|
||||
});
|
||||
|
||||
test("K1 connection status is green only for a reachable matching lease", () => {
|
||||
const unreachableLease = {
|
||||
k1_ip: "192.168.68.50",
|
||||
connection_mode: "bridge",
|
||||
connection_verification: {
|
||||
lease_state: "disconnected",
|
||||
network_reachability: "unreachable",
|
||||
},
|
||||
};
|
||||
const reachableLease = {
|
||||
...unreachableLease,
|
||||
connection_verification: {
|
||||
lease_state: "reachable",
|
||||
network_reachability: "reachable",
|
||||
},
|
||||
};
|
||||
|
||||
assert.equal(lifecycle.isReachableConnectionLease(unreachableLease, "bridge"), false);
|
||||
assert.equal(lifecycle.isReachableConnectionLease(reachableLease, "bridge"), true);
|
||||
assert.equal(lifecycle.isReachableConnectionLease(reachableLease, "quick-connect"), false);
|
||||
assert.equal(lifecycle.isReachableConnectionLease({
|
||||
k1_ip: "192.168.68.50",
|
||||
connection_mode: "bridge",
|
||||
}, "bridge"), false);
|
||||
});
|
||||
|
||||
test("provisioning intent keeps one idempotency key and exposes unsafe outcomes", () => {
|
||||
let created = 0;
|
||||
const createUuid = () => {
|
||||
@@ -751,3 +817,52 @@ test("rejected network-profile writes use safe operator copy", () => {
|
||||
);
|
||||
assert.doesNotMatch(message, /Bleak|GATT|ATT/i);
|
||||
});
|
||||
|
||||
test("host Wi-Fi helper failures do not fabricate a missing-password diagnosis", () => {
|
||||
const operationTimeout = networkProvisionFailureMessage({
|
||||
status: "failed",
|
||||
error: { code: "host-wifi-operation-timeout" },
|
||||
});
|
||||
const buildTimeout = networkProvisionFailureMessage({
|
||||
status: "failed",
|
||||
error: {
|
||||
code: "host-wifi-helper-build-timeout",
|
||||
side_effect_status: "none",
|
||||
safe_to_retry: true,
|
||||
},
|
||||
});
|
||||
const buildFailed = networkProvisionFailureMessage({
|
||||
status: "failed",
|
||||
error: {
|
||||
code: "host-wifi-helper-build-failed",
|
||||
side_effect_status: "none",
|
||||
safe_to_retry: true,
|
||||
},
|
||||
});
|
||||
const postWriteBuildFailed = networkProvisionFailureMessage({
|
||||
status: "failed",
|
||||
error: {
|
||||
code: "host-wifi-helper-build-failed",
|
||||
side_effect_status: "confirmed",
|
||||
safe_to_retry: false,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
operationTimeout,
|
||||
"Локальная операция подготовки Wi‑Fi не завершилась вовремя. Это могло произойти до изменения состояния K1; наличие сохранённого пароля этим кодом не подтверждается и не опровергается. Проверьте состояние K1 и повторите подключение отдельным действием.",
|
||||
);
|
||||
assert.doesNotMatch(operationTimeout, /получите пароль|пароль отсутствует/i);
|
||||
assert.equal(
|
||||
buildTimeout,
|
||||
"Локальный компонент Wi‑Fi не успел собраться за отведённое время. Команда K1 не отправлялась; подготовьте локальный компонент и повторите подключение отдельным действием.",
|
||||
);
|
||||
assert.equal(
|
||||
buildFailed,
|
||||
"Локальный компонент Wi‑Fi не удалось собрать. Команда K1 не отправлялась; подготовьте локальный компонент и повторите подключение отдельным действием.",
|
||||
);
|
||||
assert.equal(
|
||||
postWriteBuildFailed,
|
||||
"Локальный компонент Wi‑Fi не удалось собрать уже после начала операции с K1. Состояние устройства нельзя выводить из этой локальной ошибки; автоматического повтора команды не было. Выполните read-only проверку K1 перед новым подключением.",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -167,12 +167,6 @@ test("K1 Bridge adoption remains one explicit read-only plugin action", () => {
|
||||
join(pluginFrontendRoot, "components/K1ProvisioningPipeline.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const selectionEffectStart = provisioning.indexOf("useEffect(() => {");
|
||||
const selectionEffectEnd = provisioning.indexOf(
|
||||
"useEffect(() => {",
|
||||
selectionEffectStart + 1,
|
||||
);
|
||||
const selectionEffect = provisioning.slice(selectionEffectStart, selectionEffectEnd);
|
||||
assert.match(provisioning, /connectionMode === "bridge"/);
|
||||
assert.match(provisioning, /Подхватить существующее подключение/);
|
||||
assert.match(provisioning, /pendingAction === "verify"/);
|
||||
@@ -181,9 +175,23 @@ test("K1 Bridge adoption remains one explicit read-only plugin action", () => {
|
||||
/compatibility_attestation: profileSelectionForConnectionMode\("bridge"\)/,
|
||||
);
|
||||
assert.match(provisioning, /без изменения настроек Wi‑Fi/);
|
||||
assert.match(provisioning, /deviceSummary !== undefined/);
|
||||
assert.match(selectionEffect, /!state\.devices\.some/);
|
||||
assert.match(selectionEffect, /setSelectedDeviceId\(""\)/);
|
||||
assert.match(provisioning, /deviceSummary !== null/);
|
||||
assert.match(provisioning, /device_id: deviceSummary\.device_id/);
|
||||
});
|
||||
|
||||
test("K1 provisioning keeps the operator draft separate from the backend lease", () => {
|
||||
const provisioning = readFileSync(
|
||||
join(pluginFrontendRoot, "components/K1ProvisioningPipeline.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
assert.doesNotMatch(provisioning, /setSelectedDeviceId\(state\.selected_device_id\)/);
|
||||
assert.doesNotMatch(provisioning, /setConnectionMode\(state\.connection_mode\)/);
|
||||
assert.match(provisioning, /canSubmitProvisioningMutation\(\{/);
|
||||
assert.match(provisioning, /isReachableConnectionLease\(state, connectionMode\)/);
|
||||
assert.doesNotMatch(provisioning, /else if \(connectionMode === "quick-connect"\)/);
|
||||
assert.match(provisioning, /const succeeded = await verifyConnection\(\{/);
|
||||
assert.match(provisioning, /if \(succeeded\) \{\s*provisioningIntentRef\.current = null;/);
|
||||
});
|
||||
|
||||
test("generic Control Station has one composition import and no K1 implementation knowledge", () => {
|
||||
|
||||
Reference in New Issue
Block a user