decoder reconnect

This commit is contained in:
larssand
2026-09-09 08:08:56 +02:00
parent 09524944dd
commit d18cd7fb5e
5 changed files with 74 additions and 2 deletions

View File

@@ -0,0 +1,59 @@
import assert from "node:assert/strict";
import { startFakeDecoder } from "./browser_fake_decoder.js";
export async function runDecoderReconnectSmoke({ evaluate, navigate, savedState, waitFor, pause, base }) {
const patchState = async patch => {
const current = await savedState();
Object.assign(current, patch);
const response = await fetch(base + "/api/state", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(current) });
assert.equal(response.ok, true);
};
const badgeOnline = () => evaluate('document.getElementById("connectionBadge")?.classList.contains("badge-online")');
const decoder = await startFakeDecoder();
try {
const seeded = await savedState();
await patchState({ settings: { ...seeded.settings, autoReconnect: true, wsUrl: `ws://127.0.0.1:${decoder.port}` } });
await navigate("events", "eventForm");
await waitFor(async () => await badgeOnline(), "decoder connects on page load");
assert.equal(decoder.activeConnections(), 1, "exactly one live connection after the initial connect");
assert.equal(decoder.totalConnections(), 1, "a clean initial connect makes exactly one connection attempt");
// Simulate a real network/decoder interruption: drop the TCP connection with a raw
// reset rather than a clean WS close handshake, the way a lost cable or a decoder
// power-cycle actually would - see src/decoder_runtime.js's wsClient.onclose.
decoder.dropConnections();
await waitFor(async () => (await badgeOnline()) === false, "badge shows offline once the drop is detected");
assert.equal(decoder.activeConnections(), 0, "no lingering connection right after the drop");
// decoder_runtime.js schedules exactly one reconnect attempt 2000ms after onclose.
await waitFor(async () => decoder.totalConnections() >= 2, "a reconnect attempt reaches the decoder socket");
await waitFor(async () => await badgeOnline(), "badge shows online again after auto-reconnect");
assert.equal(decoder.totalConnections(), 2, "auto-reconnect made exactly one new connection attempt, not several");
assert.equal(decoder.activeConnections(), 1, "auto-reconnect settles on exactly one live connection, not a duplicate");
console.log("PASS decoder auto-reconnects after a dropped socket onto exactly one connection");
// A second drop must not leave a stray reconnect timer running once autoReconnect is
// turned off - the retry loop should stop, not silently keep firing in the background.
// Toggling it through the real Settings form (not a raw state patch) matters here:
// a raw backend patch never reaches this already-loaded page's in-memory state, only
// a fresh navigation would, and that would also drop and reopen the live connection.
await navigate("settings", "settingsForm");
await waitFor(async () => await badgeOnline(), "decoder is connected on the settings page");
await evaluate('document.querySelector("#settingsForm [name=autoReconnect]").checked = false');
await evaluate('document.getElementById("settingsForm").requestSubmit()');
const connectionsBeforeDrop = decoder.totalConnections();
decoder.dropConnections();
await waitFor(async () => (await badgeOnline()) === false, "badge shows offline after the second drop");
await pause(3000);
assert.equal(decoder.totalConnections(), connectionsBeforeDrop, "no reconnect attempt is made once autoReconnect is disabled");
assert.equal(await badgeOnline(), false, "stays offline with autoReconnect disabled");
assert.equal(decoder.activeConnections(), 0, "no duplicate or stray connection appears while disabled");
console.log("PASS disabling auto-reconnect stops further reconnect attempts after a drop");
} finally {
const cleanup = await savedState();
await patchState({ settings: { ...cleanup.settings, autoReconnect: false } });
await decoder.close();
}
}