/** * PresenceTracker (TASK-0093, doc ยง5) โ€” the room-scoped peer state + * broadcast shaping (server-stamped identity, the > 50 cap strip, the * 90 s prune). Injected clock (deterministic, AGENTS rule 25). */ import { describe, expect, it } from "vitest"; import { presenceColor } from "@factory/shared/presence"; import { PresenceTracker } from "../src/presence.js"; const T0 = 1_700_000_000_000; describe("PresenceTracker (TASK-0093)", () => { it("upserts a peer with a server-stamped identity (color from the shared hash)", () => { let t = T0; const p = new PresenceTracker({ now: () => t }); const entry = p.upsert( "page:pg1", { id: "u-ana", name: "Ana" }, { cursor: { blockId: "b3", offset: 4 } }, ); expect(entry.userId).toBe("u-ana"); expect(entry.name).toBe("Ana"); expect(entry.color).toBe(presenceColor("u-ana")); expect(entry.cursor).toEqual({ blockId: "b3", offset: 4 }); expect(entry.viewing).toBe(true); // default expect(entry.lastSeen).toBe(T0); expect(p.count("page:pg1")).toBe(1); }); it("updates in place (a cursor move keeps the entry; an omitted field keeps the prior value)", () => { let t = T0; const p = new PresenceTracker({ now: () => t }); p.upsert( "page:pg1", { id: "u-ana", name: "Ana" }, { cursor: { blockId: "b1", offset: 0 }, viewing: false }, ); t += 1000; const e = p.upsert( "page:pg1", { id: "u-ana", name: "Ana" }, { cursor: { blockId: "b2", offset: 3 } }, ); expect(e.cursor).toEqual({ blockId: "b2", offset: 3 }); expect(e.viewing).toBe(false); // kept from the prior upsert expect(e.lastSeen).toBe(T0 + 1000); expect(p.count("page:pg1")).toBe(1); // the same (room, user) key }); it("a cursor clear (null) is distinct from an omitted cursor", () => { const p = new PresenceTracker({ now: () => T0 }); p.upsert("page:pg1", { id: "u-ana" }, { cursor: { blockId: "b1", offset: 1 } }); const cleared = p.upsert("page:pg1", { id: "u-ana" }, { cursor: null }); expect(cleared.cursor).toBeNull(); const kept = p.upsert("page:pg1", { id: "u-ana" }, {}); expect(kept.cursor).toBeNull(); // still the cleared value }); it("separates rooms and users; removeRoom + remove clean up", () => { const p = new PresenceTracker({ now: () => T0 }); p.upsert("page:pg1", { id: "u-a" }, {}); p.upsert("page:pg1", { id: "u-b" }, {}); p.upsert("page:pg2", { id: "u-a" }, {}); // same user, other page expect(p.count("page:pg1")).toBe(2); expect(p.count("page:pg2")).toBe(1); expect(p.size).toBe(3); p.remove("page:pg1", "u-a"); expect(p.count("page:pg1")).toBe(1); p.removeRoom("page:pg2"); expect(p.size).toBe(1); }); it("falls back to the shared guest name (color is never the sole identifier)", () => { const p = new PresenceTracker({ now: () => T0 }); const e = p.upsert("page:pg1", { id: "u-ghost" }, {}); expect(e.name).toBe("Guest (shared)"); expect(e.color).toBe(presenceColor("u-ghost")); }); it("broadcastFor stamps the frame with the room key in pageId", () => { const p = new PresenceTracker({ now: () => T0 }); const e = p.upsert( "page:pg1", { id: "u-ana", name: "Ana" }, { cursor: { blockId: "b3", offset: 4 } }, ); const f = p.broadcastFor("page:pg1", e); expect(f).toEqual({ type: "presence", pageId: "page:pg1", user: { id: "u-ana", name: "Ana", color: presenceColor("u-ana") }, cursor: { blockId: "b3", offset: 4 }, viewing: true, lastSeen: T0, }); }); it("at exactly the cap (50 peers) the full cursor is still carried", () => { const p = new PresenceTracker({ now: () => T0, peerCap: 3 }); for (let i = 0; i < 3; i += 1) p.upsert("page:pg1", { id: `u-${i}` }, {}); const e = p.upsert( "page:pg1", { id: "u-hot", name: "Hot" }, { cursor: { blockId: "b1", offset: 0 } }, ); // count is now 4 > cap 3 โ†’ stripped const f = p.broadcastFor("page:pg1", e); expect(f.cursor).toBeNull(); expect(f.viewing).toBeUndefined(); }); it("over the cap the broadcast is avatars-only (cursor/viewing stripped, user stays)", () => { const p = new PresenceTracker({ now: () => T0, peerCap: 3 }); for (let i = 0; i < 4; i += 1) p.upsert( "page:pg1", { id: `u-${i}`, name: `U${i}` }, { cursor: { blockId: "b1", offset: i } }, ); const e = p.peers("page:pg1")![3]!; const f = p.broadcastFor("page:pg1", e); expect(f.cursor).toBeNull(); expect(f.viewing).toBeUndefined(); expect(f.user).toEqual({ id: e.userId, name: e.name, color: e.color }); }); it("dropping back under the cap carries full cursors again (the strip is per-broadcast, not sticky)", () => { const p = new PresenceTracker({ now: () => T0, peerCap: 3 }); for (let i = 0; i < 4; i += 1) p.upsert("page:pg1", { id: `u-${i}` }, {}); p.remove("page:pg1", "u-0"); // 3 = cap โ†’ full const e = p.peers("page:pg1")![0]!; e.cursor = { blockId: "b9", offset: 7 }; const f = p.broadcastFor("page:pg1", e); expect(f.cursor).toEqual({ blockId: "b9", offset: 7 }); expect(f.viewing).toBe(true); }); it("prune drops entries silent longer than the threshold (default 90 s)", () => { let t = T0; const p = new PresenceTracker({ now: () => t }); // default pruneAfterMs = 90 s p.upsert("page:pg1", { id: "u-a" }, {}); p.upsert("page:pg1", { id: "u-b" }, {}); t += 60_000; p.upsert("page:pg1", { id: "u-b" }, {}); // u-b refreshed; u-a is 60 s stale expect(p.prune().size).toBe(0); // u-a at 60 s < 90 s t += 31_000; // u-a now 91 s stale; u-b 31 s (survives) const pruned = p.prune(); expect([...pruned.keys()]).toEqual(["page:pg1"]); expect(pruned.get("page:pg1")?.map((e) => e.userId)).toEqual(["u-a"]); expect(p.count("page:pg1")).toBe(1); // u-b survives }); it("prune with a custom threshold (the integration test's short window)", () => { let t = T0; const p = new PresenceTracker({ now: () => t, pruneAfterMs: 5_000 }); p.upsert("page:pg1", { id: "u-a" }, {}); t += 5_001; expect([...p.prune().values()].flat().map((e) => e.userId)).toEqual(["u-a"]); }); });