On disconnect, a player's slot is held for 60s via ReconnectManager. During the grace window, game.delta frames destined for the absent slot are buffered in order. If the client reconnects with its original token (envelope-level), the grace timer is cancelled and the server replays a fresh game.state snapshot plus every buffered delta. Timer expiry triggers the original "player_left" game.end cleanup that previously ran immediately on disconnect. - new packages/server/src/reconnect.ts: ReconnectManager (no WS refs, no registry coupling, unref'd timers so tests don't block exit) - broadcast.ts: unregisterConnection starts grace; handleRoomJoin routes reconnect path via envelope token + isPending check; handleGameMove buffers deltas for disconnected opponents - reconnect.test.ts: 9 unit cases (grace/cancel/buffer/expire/reset) - broadcast.test.ts: end-to-end reconnect scenario + negative case
75 lines
2.5 KiB
TypeScript
75 lines
2.5 KiB
TypeScript
import { beforeEach, describe, expect, test } from "vitest";
|
|
|
|
import {
|
|
__resetForTests,
|
|
incMessages,
|
|
incMovesFail,
|
|
incMovesOk,
|
|
recordTickDuration,
|
|
renderMetrics,
|
|
setActiveRooms,
|
|
} from "./logging.js";
|
|
|
|
beforeEach(() => {
|
|
__resetForTests();
|
|
});
|
|
|
|
describe("renderMetrics — Prometheus text format", () => {
|
|
test("initial state produces zero-valued metrics", () => {
|
|
const out = renderMetrics();
|
|
expect(out).toContain("rooms_active 0");
|
|
expect(out).toContain("messages_received_total 0");
|
|
expect(out).toContain('moves_validated_total{result="ok"} 0');
|
|
expect(out).toContain('moves_validated_total{result="fail"} 0');
|
|
expect(out).toContain("tick_duration_milliseconds_sum 0");
|
|
expect(out).toContain("tick_duration_milliseconds_count 0");
|
|
});
|
|
|
|
test("incMessages increments messages_received_total", () => {
|
|
incMessages();
|
|
incMessages();
|
|
expect(renderMetrics()).toContain("messages_received_total 2");
|
|
});
|
|
|
|
test("incMovesOk / incMovesFail are independent counters", () => {
|
|
incMovesOk();
|
|
incMovesOk();
|
|
incMovesOk();
|
|
incMovesFail();
|
|
const out = renderMetrics();
|
|
expect(out).toContain('moves_validated_total{result="ok"} 3');
|
|
expect(out).toContain('moves_validated_total{result="fail"} 1');
|
|
});
|
|
|
|
test("setActiveRooms updates the gauge", () => {
|
|
setActiveRooms(4);
|
|
expect(renderMetrics()).toContain("rooms_active 4");
|
|
setActiveRooms(1);
|
|
expect(renderMetrics()).toContain("rooms_active 1");
|
|
});
|
|
|
|
test("recordTickDuration accumulates sum and count", () => {
|
|
recordTickDuration(10);
|
|
recordTickDuration(20);
|
|
recordTickDuration(30);
|
|
const out = renderMetrics();
|
|
expect(out).toContain("tick_duration_milliseconds_sum 60");
|
|
expect(out).toContain("tick_duration_milliseconds_count 3");
|
|
});
|
|
|
|
test("output includes required HELP and TYPE lines", () => {
|
|
const out = renderMetrics();
|
|
expect(out).toContain("# HELP rooms_active");
|
|
expect(out).toContain("# TYPE rooms_active gauge");
|
|
expect(out).toContain("# HELP messages_received_total");
|
|
expect(out).toContain("# TYPE messages_received_total counter");
|
|
expect(out).toContain("# HELP moves_validated_total");
|
|
expect(out).toContain("# TYPE moves_validated_total counter");
|
|
expect(out).toContain("# HELP tick_duration_milliseconds");
|
|
expect(out).toContain("# TYPE tick_duration_milliseconds summary");
|
|
});
|
|
|
|
test("output ends with a trailing newline (Prometheus spec)", () => {
|
|
expect(renderMetrics().endsWith("\n")).toBe(true);
|
|
});
|
|
});
|