trails/apps/planner/app/lib/yjs-server.test.ts
Ullrich Schäfer 0553cf4a5e
Fix planner_active_sessions gauge drifting negative
The gauge was maintained by conditional inc on "this sessionId isn't
in the in-memory `docs` Map yet" and dec on "last client for the
sessionId left." Because `docs` caches entries indefinitely — even
after every client disconnects — a reconnect found the doc still
cached, skipped the inc, then decremented again on the next
disconnect. Every reconnect-after-last-disconnect leaked one
decrement. After a day of normal reload / wifi-blip / tab-hibernate
patterns the gauge hit -182.

Replace the transitions with a `set()` derived from the live `conns`
map: count distinct session ids with at least one open WebSocket.
Self-correcting, no drift possible.

Extract `countActiveSessions()` as a pure helper + unit tests
including a regression case that simulates repeated reconnect/
disconnect cycles on one session and asserts the count never goes
negative.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 22:37:58 +02:00

45 lines
1.5 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { countActiveSessions } from "./yjs-server.ts";
describe("countActiveSessions", () => {
it("is 0 for an empty iterable (regression: prod gauge saw -182 drift)", () => {
expect(countActiveSessions([])).toBe(0);
});
it("counts a single session with one client as 1", () => {
expect(countActiveSessions([{ sessionId: "s1" }])).toBe(1);
});
it("deduplicates multiple clients on the same session", () => {
expect(
countActiveSessions([
{ sessionId: "s1" },
{ sessionId: "s1" },
{ sessionId: "s1" },
]),
).toBe(1);
});
it("counts distinct sessions across clients", () => {
expect(
countActiveSessions([
{ sessionId: "s1" },
{ sessionId: "s2" },
{ sessionId: "s1" },
{ sessionId: "s3" },
]),
).toBe(3);
});
it("doesn't go negative under the reconnect pattern that broke the old inc/dec logic", () => {
// Simulate: client connects, client disconnects, reconnects,
// disconnects again — five times over. The old code would have
// decremented once per cycle (because `docs` stayed cached, so
// the re-connect's `isNewSession` check skipped inc). Here we
// derive from live state, so each empty snapshot is 0, not -N.
for (let i = 0; i < 5; i++) {
expect(countActiveSessions([{ sessionId: "s1" }])).toBe(1); // reconnect
expect(countActiveSessions([])).toBe(0); // disconnect
}
});
});