trails/apps/planner/app/lib/host-election.test.ts
Ullrich Schäfer 55806fc84a
Fix waypoint drag, host election, segment routing, debug panel
Waypoint drag:
- Use observeDeep instead of observe to detect Y.Map property changes

Host election:
- Extracted to host-election.ts with 8 unit tests
- Simple rule: lowest real client ID is always host, deterministic
- Always sets role (host or participant), no missing roles
- Filters server docs (empty awareness state) from election

BRouter routing:
- Route segment-by-segment like bikerouter.de (one request per
  waypoint pair), guarantees route passes through every waypoint
- Merge GeoJSON segments with accumulated stats
- 8 unit tests for segment merging and waypoint splitting

Awareness cleanup:
- Track client IDs per WebSocket connection
- Immediately remove awareness on WebSocket close

Debug panel:
- Visibility + expanded state persisted in localStorage
- Hotkey (Shift+Cmd+Option+T) toggles visibility
- Labels server docs as (server)
- All logic self-contained in YjsDebugPanel.tsx

User identity:
- Color and name persisted in localStorage across reloads

Tests: 32 total (16 existing + 8 host election + 8 routing)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 08:52:52 +01:00

94 lines
3.2 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { electHost } from "./host-election";
function makeStates(
entries: Array<{ id: number; user?: boolean; role?: string }>,
): Map<number, Record<string, unknown>> {
const map = new Map<number, Record<string, unknown>>();
for (const e of entries) {
const state: Record<string, unknown> = {};
if (e.user) state.user = { color: "#000", name: "Test" };
if (e.role) state.role = e.role;
map.set(e.id, state);
}
return map;
}
describe("electHost", () => {
it("elects the only real client as host", () => {
const states = makeStates([
{ id: 100, user: true },
]);
expect(electHost(states, 100)).toEqual({ isHost: true, role: "host" });
});
it("elects lowest ID among two real clients", () => {
const states = makeStates([
{ id: 200, user: true },
{ id: 100, user: true },
]);
expect(electHost(states, 100)).toEqual({ isHost: true, role: "host" });
expect(electHost(states, 200)).toEqual({ isHost: false, role: "participant" });
});
it("ignores server docs (no user state)", () => {
const states = makeStates([
{ id: 50 }, // server doc — lowest ID but no user
{ id: 200, user: true },
{ id: 100, user: true },
]);
expect(electHost(states, 100)).toEqual({ isHost: true, role: "host" });
expect(electHost(states, 200)).toEqual({ isHost: false, role: "participant" });
});
it("always assigns a role (never undefined)", () => {
const states = makeStates([
{ id: 300, user: true },
{ id: 100, user: true },
{ id: 200, user: true },
]);
const result = electHost(states, 200);
expect(result.role).toBeDefined();
expect(result.role).toBe("participant");
});
it("handles single client with server doc", () => {
const states = makeStates([
{ id: 999 }, // server
{ id: 500, user: true },
]);
expect(electHost(states, 500)).toEqual({ isHost: true, role: "host" });
});
it("handles no real clients (defensive)", () => {
const states = makeStates([
{ id: 10 }, // server only
]);
// localId not in states — still returns host as fallback
expect(electHost(states, 42)).toEqual({ isHost: true, role: "host" });
});
it("ignores existing role claims — always uses lowest ID", () => {
const states = makeStates([
{ id: 200, user: true, role: "host" },
{ id: 100, user: true, role: "participant" },
]);
// ID 100 should be host regardless of current role claims
expect(electHost(states, 100)).toEqual({ isHost: true, role: "host" });
expect(electHost(states, 200)).toEqual({ isHost: false, role: "participant" });
});
it("works with many clients", () => {
const states = makeStates([
{ id: 999, user: true },
{ id: 500, user: true },
{ id: 42 }, // server
{ id: 200, user: true },
{ id: 100, user: true },
{ id: 800, user: true },
]);
expect(electHost(states, 100)).toEqual({ isHost: true, role: "host" });
expect(electHost(states, 500)).toEqual({ isHost: false, role: "participant" });
expect(electHost(states, 999)).toEqual({ isHost: false, role: "participant" });
});
});