Files
NODEDC_MISSION_CORE/apps/control-station/test/networkProvisioning.test.mjs
T

134 lines
5.7 KiB
JavaScript

import assert from "node:assert/strict";
import { before, after, test } from "node:test";
import { createServer } from "vite";
let server;
let observeProvisioningRequest;
let networkProvisionFailureMessage;
let ApiError;
before(async () => {
server = await createServer({ appType: "custom", logLevel: "silent", server: { middlewareMode: true } });
({ observeProvisioningRequest, networkProvisionFailureMessage } = await server.ssrLoadModule("@xgrids-k1/frontend/networkProvisioning.ts"));
({ ApiError } = await server.ssrLoadModule("@xgrids-k1/frontend/api.ts"));
});
after(async () => { await server?.close(); });
test("reviewed station errors explain Wi-Fi recovery without claiming a Bluetooth failure", () => {
for (const [code, pattern] of [
["k1-wifi-network-not-found", /указанная сеть не найдена/],
["k1-wifi-credentials-required", /устройство запросило учётные данные сети/],
]) {
const message = networkProvisionFailureMessage({ status: "failed", error: { code } });
assert.match(message, pattern);
assert.match(message, /Проверьте название сети и пароль/);
assert.doesNotMatch(message, /INVALID_PDU|ошибкой Bluetooth|Переподключиться/);
}
assert.match(networkProvisionFailureMessage({ status: "failed", error: {
code: "BleakGATTProtocolError", ble_att_error_code: 4, ble_att_error_name: "INVALID_PDU",
} }), /INVALID_PDU/);
assert.match(networkProvisionFailureMessage({ status: "failed", error: {
code: "network-not-found",
} }), /macOS/);
});
const request = { device_id: "synthetic-k1", connection_mode: "bridge", idempotency_key: "explicit-one" };
function snapshot(status, revision = 2) {
return {
snapshot_runtime_id: "runtime-one",
snapshot_runtime_started_at_utc: "2026-09-06T00:00:00Z",
snapshot_revision: revision,
operations: status ? [{
operation_id: "op-one", action: "network.provision", idempotency_key: request.idempotency_key,
status, error: status === "failed" ? {
code: "BleakGATTProtocolError", ble_att_error_code: 4, ble_att_error_name: "INVALID_PDU",
device_write_attempted: true, device_write_confirmed: false,
safe_to_retry: false, side_effect_status: "unknown",
} : null,
}] : [],
};
}
function context(overrides = {}) {
let current = snapshot(null, 1);
const calls = [];
return {
calls,
initialState: current,
send: async (input) => { calls.push(["send", input]); throw new ApiError("transport", 502); },
readState: async () => { calls.push(["read"]); return snapshot("failed"); },
acceptState: (incoming) => { if (incoming.snapshot_revision >= current.snapshot_revision) current = incoming; },
currentState: () => current,
assertCurrent() {},
...overrides,
};
}
test("HTTP 502 retains the exact failed snapshot and ATT facts after one submission", async () => {
const ctx = context();
const result = await observeProvisioningRequest(request, ctx);
assert.deepEqual(ctx.calls.map(([kind]) => kind), ["send", "read"]);
assert.equal(ctx.calls[0][1], request);
assert.equal(result.operation.operation_id, "op-one");
assert.equal(result.operation.error.ble_att_error_name, "INVALID_PDU");
assert.equal(result.state.operations[0], result.operation);
assert.equal(result.transportError.status, 502);
});
test("an existing failed or successful intent is never resubmitted", async () => {
for (const status of ["failed", "succeeded"]) {
const ctx = context({ initialState: snapshot(status) });
const result = await observeProvisioningRequest(request, ctx);
assert.equal(result.operation.status, status);
assert.equal(ctx.calls.length, 0);
}
});
test("lost HTTP response can observe journal success without another command", async () => {
const ctx = context({ readState: async () => snapshot("succeeded") });
const result = await observeProvisioningRequest(request, ctx);
assert.equal(result.operation.status, "succeeded");
assert.equal(ctx.calls.filter(([kind]) => kind === "send").length, 1);
});
test("an admitted pending intent is followed only by bounded local state reads", async () => {
let now = 0;
const ctx = context({
initialState: snapshot("running"),
settlementOptions: { now: () => now, wait: async (ms) => { now += ms; } },
});
const result = await observeProvisioningRequest(request, ctx);
assert.equal(result.operation.status, "failed");
assert.deepEqual(ctx.calls.map(([kind]) => kind), ["read"]);
});
test("newer WebSocket failure outranks a stale running REST result", async () => {
const newer = snapshot("failed", 7);
const ctx = context({
send: async () => snapshot("running", 5),
currentState: () => newer,
});
const result = await observeProvisioningRequest(request, ctx);
assert.equal(result.state, newer);
assert.equal(result.operation.status, "failed");
assert.equal(ctx.calls.length, 0);
});
test("superseding an intent after dispatch stops continuation without replay", async () => {
let active = true;
const ctx = context({
send: async () => { active = false; throw new ApiError("lost", 502); },
assertCurrent: () => { if (!active) throw new Error("superseded"); },
});
await assert.rejects(observeProvisioningRequest(request, ctx), /superseded/);
assert.equal(ctx.calls.length, 0);
});
test("an unrelated journal row cannot confirm the current command", async () => {
const unrelated = snapshot("succeeded");
unrelated.operations[0].idempotency_key = "another-intent";
const ctx = context({ readState: async () => unrelated });
const result = await observeProvisioningRequest(request, ctx);
assert.equal(result.operation, null);
assert.equal(result.transportError.status, 502);
assert.equal(ctx.calls.length, 1);
});