houserules/packages/server/src/logging.test.ts
Joey Yakimowich-Payne 2d0b8399f8
feat(server): add reconnection with 60s grace + snapshot resume (P4.7)
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
2026-04-16 17:38:26 -06:00

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);
});
});