// Phase 28 — MailStore threading projection tests. Exercises the // `entries` derived rune end-to-end with handcrafted inbox + sent // fixtures; the network surface (GalaxyClient) is left null since // these tests do not call init / setGame. import { describe, expect, it } from "vitest"; import { MailStore, type MailListEntry, type MailStandalone, type MailThread, } from "../src/lib/mail-store.svelte"; import type { MailMessage } from "../src/api/diplomail"; function makeMessage(overrides: Partial): MailMessage { return { messageId: overrides.messageId ?? crypto.randomUUID(), gameId: overrides.gameId ?? "00000000-0000-0000-0000-000000000001", gameName: overrides.gameName ?? "Test game", kind: overrides.kind ?? "personal", senderKind: overrides.senderKind ?? "player", senderUserId: overrides.senderUserId ?? null, senderUsername: overrides.senderUsername ?? null, senderRaceName: overrides.senderRaceName ?? null, subject: overrides.subject ?? "", body: overrides.body ?? "", bodyLang: overrides.bodyLang ?? "en", broadcastScope: overrides.broadcastScope ?? "single", createdAt: overrides.createdAt ?? new Date(0), recipientUserId: overrides.recipientUserId ?? "00000000-0000-0000-0000-000000000099", recipientUserName: overrides.recipientUserName ?? "self", recipientRaceName: overrides.recipientRaceName ?? null, readAt: overrides.readAt ?? null, deletedAt: overrides.deletedAt ?? null, translatedSubject: overrides.translatedSubject ?? null, translatedBody: overrides.translatedBody ?? null, translationLang: overrides.translationLang ?? null, translator: overrides.translator ?? null, }; } describe("MailStore.entries", () => { it("groups inbox + sent messages into per-race threads", () => { const store = new MailStore(); const greysIncoming = makeMessage({ messageId: "m1", senderKind: "player", senderRaceName: "Greys", body: "hi", createdAt: new Date("2026-05-15T12:00:00Z"), }); const greysOutgoing = makeMessage({ messageId: "m2", senderKind: "player", senderRaceName: "Self", recipientRaceName: "Greys", body: "reply", createdAt: new Date("2026-05-15T12:05:00Z"), }); store.inbox = [greysIncoming]; store.sent = [greysOutgoing]; const entries = store.entries; expect(entries.length).toBe(1); const entry = entries[0]; expect(entry.kind).toBe("thread"); const thread = entry as MailThread; expect(thread.raceName).toBe("Greys"); expect(thread.messages.map((m) => m.messageId)).toEqual(["m1", "m2"]); }); it("surfaces system and admin messages as stand-alone items", () => { const store = new MailStore(); const system = makeMessage({ messageId: "sys-1", kind: "admin", senderKind: "system", subject: "game.paused: details", createdAt: new Date("2026-05-15T13:00:00Z"), }); const admin = makeMessage({ messageId: "adm-1", kind: "admin", senderKind: "admin", createdAt: new Date("2026-05-15T13:05:00Z"), }); store.inbox = [system, admin]; const entries: MailListEntry[] = store.entries; expect(entries.every((e) => e.kind === "standalone")).toBe(true); const standalones = entries as MailStandalone[]; expect(standalones.map((e) => e.message.messageId).sort()).toEqual( ["adm-1", "sys-1"], ); }); it("treats the local player's broadcasts as stand-alone outgoing", () => { const store = new MailStore(); const broadcast = makeMessage({ messageId: "br-1", senderKind: "player", senderRaceName: "Self", recipientRaceName: "Greys", broadcastScope: "game_broadcast", createdAt: new Date("2026-05-15T14:00:00Z"), }); store.sent = [broadcast]; const entries = store.entries; expect(entries.length).toBe(1); expect(entries[0].kind).toBe("standalone"); }); it("counts unread incoming messages in unreadCount", () => { const store = new MailStore(); store.inbox = [ makeMessage({ readAt: null, createdAt: new Date(1) }), makeMessage({ readAt: new Date(2), createdAt: new Date(2) }), makeMessage({ readAt: null, createdAt: new Date(3) }), ]; expect(store.unreadCount).toBe(2); }); });