Phase 28 (Step 11): Vitest coverage for MailStore threading
`tests/mail-store.test.ts` exercises the `entries` derived rune with handcrafted inbox + sent fixtures: - personal messages exchanged with one race collapse into a per-race thread with messages sorted oldest → newest; - system mail (`sender_kind=system`) and admin notifications (`sender_kind=admin`) surface as stand-alone items even when a race-name snapshot is present; - the caller's own paid-tier broadcasts (`broadcast_scope= game_broadcast`) render as stand-alone outgoing items; - `unreadCount` counts inbox rows with `readAt === null`. The store fields are mutated directly to avoid wiring a fake `GalaxyClient`; the underlying `$derived` rune fires whenever those fields change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
|||||||
|
// 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>): 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user