fix(k1): harden BLE discovery and bridge recovery

This commit is contained in:
DCCONSTRUCTIONS
2026-08-06 12:43:00 +03:00
parent be50144ca8
commit 52da9b75b7
14 changed files with 1010 additions and 48 deletions
@@ -19,6 +19,7 @@ let presentation;
let operatorIntentGeneration;
let configuration;
let compatibility;
let networkProvisionFailureMessage;
before(async () => {
server = await createServer({
@@ -62,6 +63,9 @@ before(async () => {
({ localizeRuntimeMessage } = await server.ssrLoadModule(
"@xgrids-k1/frontend/messages.ts",
));
({ networkProvisionFailureMessage } = await server.ssrLoadModule(
"@xgrids-k1/frontend/useXgridsK1Runtime.ts",
));
});
after(async () => {
@@ -216,6 +220,7 @@ test("installed XGRIDS frontend manifest exposes the semantic v1alpha2 actions",
assert.equal(xgridsK1Actions.acquisitionPrepare, "acquisition.prepare");
assert.equal(xgridsK1Actions.acquisitionStart, "acquisition.start");
assert.equal(xgridsK1Actions.acquisitionStop, "acquisition.stop");
assert.equal(xgridsK1Actions.connectionVerify, "connection.verify");
});
test("one operator action can open at most one K1 control session", () => {
@@ -693,3 +698,56 @@ test("device mutations send explicit nested compatibility attestation", async ()
assert.deepEqual(prepare.input.compatibility_attestation, attestation);
assert.equal(prepare.input.project_name, "Mission 01");
});
test("connection verification supports refresh and explicit read-only adoption", async () => {
const originalFetch = globalThis.fetch;
const calls = [];
globalThis.fetch = async (path, init) => {
calls.push({ path, init });
return new Response(JSON.stringify({ state: { source_mode: "idle" } }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
};
const attestation = {
firmware_version: "3.0.2",
topology: "direct-lan",
verification: "live-device-info",
};
try {
await xgridsK1Api.verifyConnection();
await xgridsK1Api.verifyConnection({
device_id: "fresh-ble-device",
compatibility_attestation: attestation,
});
} finally {
globalThis.fetch = originalFetch;
}
assert.equal(calls.length, 2);
assert.match(String(calls[0].path), /actions\/connection\.verify$/);
assert.match(String(calls[1].path), /actions\/connection\.verify$/);
assert.deepEqual(JSON.parse(calls[0].init.body), { input: {} });
const adoption = JSON.parse(calls[1].init.body).input;
assert.deepEqual(adoption, {
device_id: "fresh-ble-device",
compatibility_attestation: attestation,
});
assert.equal("ssid" in adoption, false);
assert.equal("password" in adoption, false);
assert.equal("connection_mode" in adoption, false);
});
test("rejected network-profile writes use safe operator copy", () => {
const message = networkProvisionFailureMessage({
status: "failed",
error: { code: "BleakGATTProtocolError" },
});
assert.equal(
message,
"Сканер отклонил запись сетевого профиля. Результат изменения сети неизвестен; автоматический повтор запрещён. Проверьте текущее состояние K1 или подхватите существующее подключение без изменения настроек Wi‑Fi.",
);
assert.doesNotMatch(message, /Bleak|GATT|ATT/i);
});
@@ -162,6 +162,30 @@ test("XGRIDS frontend is physically plugin-owned and split by operator pipeline"
assert.match(spatialControls, /Повторить остановку/);
});
test("K1 Bridge adoption remains one explicit read-only plugin action", () => {
const provisioning = readFileSync(
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"/);
assert.match(
provisioning,
/compatibility_attestation: profileSelectionForConnectionMode\("bridge"\)/,
);
assert.match(provisioning, /без изменения настроек Wi‑Fi/);
assert.match(provisioning, /deviceSummary !== undefined/);
assert.match(selectionEffect, /!state\.devices\.some/);
assert.match(selectionEffect, /setSelectedDeviceId\(""\)/);
});
test("generic Control Station has one composition import and no K1 implementation knowledge", () => {
const compositionPath = join(coreSourceRoot, "composition/devicePlugins.ts");
const composition = readFileSync(compositionPath, "utf8");