fix(k1): restore canonical local connection lifecycle

This commit is contained in:
DCCONSTRUCTIONS
2026-08-06 13:52:46 +03:00
parent 52da9b75b7
commit aff331082f
19 changed files with 2169 additions and 274 deletions
@@ -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", () => {