134 lines
5.7 KiB
JavaScript
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);
|
|
});
|