import assert from "node:assert/strict"; import test from "node:test"; import { formatFailure, startCraftos, startServer, waitFor } from "./harness.js"; const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); // Regression for the reconnect ping-pong: two live daemons sharing a traposId used to // thrash forever — each newest-wins eviction (close 1000) made the displaced client // reconnect, which evicted the other, every `reconnectDelay`. The fix closes the evicted // socket with code 4000 ("displaced"); the Lua client treats that as terminal and yields. // // registry.count() can't see the churn (newest-wins pins it at 1), so we count server-side // "gateway connection opened" log lines. With the fix the loser yields, so opens stay at the // two initial connects; pre-fix this number climbed once per reconnectDelay. test("displaced daemon yields instead of reconnect-looping", async () => { const server = await startServer({ captureLogs: true }); const TRAPOS_ID = "9:dup"; // Aggressive reconnect so the pre-fix loop would be obvious within the observation window. const RECONNECT_DELAY = "1"; const first = startCraftos("gateway-client.lua", { computerId: 9, computerLabel: "dup", shellArgs: [server.baseUrl, "-", "idle", RECONNECT_DELAY], }); let second: ReturnType | undefined; try { await waitFor(() => first.snapshot().includes("__CLOUD_READY__"), 12_000, "first connected"); await waitFor(() => server.registry.has("local", TRAPOS_ID), 2_000, "first registered server-side"); const secondHandle = startCraftos("gateway-client.lua", { computerId: 9, computerLabel: "dup", shellArgs: [server.baseUrl, "-", "idle", RECONNECT_DELAY], }); second = secondHandle; await waitFor(() => secondHandle.snapshot().includes("__CLOUD_READY__"), 12_000, "second connected"); const openedAfterBoth = server.countLog("gateway connection opened"); // Let several reconnect intervals elapse. If the displaced daemon reconnected, we would // see a fresh "gateway connection opened" every ~1s here. await sleep(5_000); const openedAfterWait = server.countLog("gateway connection opened"); assert.equal( openedAfterWait, openedAfterBoth, `no new connections after both daemons settled (was ${openedAfterBoth}, now ${openedAfterWait})`, ); // The two initial connects, and nothing more. assert.ok(openedAfterWait <= 3, `bounded connection count, got ${openedAfterWait}`); assert.equal(server.registry.count(), 1, "exactly one registration survives"); // The surviving (newest) daemon still answers a gateway->computer ping. const payload = await server.registry.request("local", TRAPOS_ID, "ping", {}, 2_000); assert.deepEqual(payload, { traposId: TRAPOS_ID }, "survivor answers reverse ping"); } catch (error) { throw new Error( formatFailure( error instanceof Error ? error.message : String(error), `first:\n${first.snapshot()}\nsecond:\n${second?.snapshot() ?? "(not started)"}`, ), { cause: error }, ); } finally { first.abort(); second?.abort(); await Promise.all([first.done.catch(() => undefined), second?.done.catch(() => undefined)]); await server.close(); } });