/** * TASK-0094 (shared) — the comment-event → frame-kind mapping, the * notification policy table, and the per-type email-pref resolution. * * These are the single-source contract the API (emitter), the realtime * app (fan-out) and the worker (email) all import — drift here breaks all * three, so the tests pin the exact mapping + policy + defaults. */ import { describe, expect, it } from "vitest"; import { commentEventKindForEvent, COMMENT_EVENT_KINDS } from "./comment-events.js"; import { NOTIFICATION_POLICY, NOTIFICATION_TYPES, notificationPolicy, notificationToasts, } from "./notification-policy.js"; import { DEFAULT_NOTIFICATION_PREFS, NotificationPrefsSchema, effectivePrefMode, prefIsOff, prefSendsInstant, } from "./notification-prefs.js"; import type { DomainEvent } from "../events.js"; const evt = (name: string, payload: Record): DomainEvent => ({ id: "evt_x", name: name as DomainEvent["name"], payload, ts: "2026-01-01T00:00:00.000Z", }) as DomainEvent; describe("commentEventKindForEvent (TASK-0094 R1)", () => { it("comment.created without a replyId → 'created' (a new thread)", () => { expect( commentEventKindForEvent(evt("comment.created", { commentId: "c1", pageId: "p1" })), ).toBe("created"); }); it("comment.created WITH a replyId → 'replied' (a reply = a comment with a parent)", () => { expect( commentEventKindForEvent( evt("comment.created", { commentId: "c1", replyId: "r1", pageId: "p1" }), ), ).toBe("replied"); }); it("comment.resolved → 'resolved'", () => { expect( commentEventKindForEvent(evt("comment.resolved", { commentId: "c1", pageId: "p1" })), ).toBe("resolved"); }); it("comment.reopened → 'reopened' (additive internal event)", () => { expect( commentEventKindForEvent(evt("comment.reopened", { commentId: "c1", pageId: "p1" })), ).toBe("reopened"); }); it("comment.reaction → 'reacted' (additive internal event)", () => { expect( commentEventKindForEvent( evt("comment.reaction", { commentId: "c1", emoji: "👍", pageId: "p1" }), ), ).toBe("reacted"); }); it("comment.deleted → 'deleted' (additive internal event)", () => { expect( commentEventKindForEvent(evt("comment.deleted", { commentId: "c1", pageId: "p1" })), ).toBe("deleted"); }); it("an unrelated event → null (the mapper skips it)", () => { expect(commentEventKindForEvent(evt("page.updated", { pageId: "p1" }))).toBeNull(); expect( commentEventKindForEvent(evt("notification.created", { notificationId: "n1" })), ).toBeNull(); }); it("the kind set is exactly the frame schema's enum (lockstep)", () => { // The frame's `kind` enum (frames.ts) must equal this mapping's domain. expect(COMMENT_EVENT_KINDS).toEqual([ "created", "replied", "resolved", "reopened", "reacted", "deleted", ]); }); }); describe("notification policy table (TASK-0094 R5/R7)", () => { it("MENTION + ASSIGNMENT toast; everything else does not", () => { expect(notificationToasts("MENTION")).toBe(true); expect(notificationToasts("ASSIGNMENT")).toBe(true); expect(notificationToasts("COMMENT")).toBe(false); expect(notificationToasts("PAGE_SHARED")).toBe(false); expect(notificationToasts("DATABASE_CHANGE")).toBe(false); expect(notificationToasts("AUTOMATION")).toBe(false); expect(notificationToasts("INTEGRATION")).toBe(false); expect(notificationToasts("TASK_ASSIGNED")).toBe(false); }); it("the kind is the snake_case type (the frame's `kind` field)", () => { expect(notificationPolicy("MENTION").kind).toBe("mention"); expect(notificationPolicy("PAGE_SHARED").kind).toBe("page_shared"); expect(notificationPolicy("DATABASE_CHANGE").kind).toBe("database_change"); }); it("covers every NotificationType exactly once (lockstep with Prisma)", () => { expect([...NOTIFICATION_TYPES].sort()).toEqual([ "APPROVAL_DECISION", "ASSIGNMENT", "AUTOMATION", "COMMENT", "DATABASE_CHANGE", "FORM_SUBMITTED", "INTEGRATION", "MENTION", "PAGE_SHARED", "REVIEW_REQUESTED", "TASK_ASSIGNED", "TASK_DUE", "WATCH_UPDATE", ]); for (const t of NOTIFICATION_TYPES) { expect(NOTIFICATION_POLICY[t]).toBeDefined(); } }); it("an unknown type → conservative policy (no toast, kind = lower-case type) — never a crash", () => { const p = notificationPolicy("SOME_FUTURE_TYPE"); expect(p.toast).toBe(false); expect(p.kind).toBe("some_future_type"); }); }); describe("notification email prefs (TASK-0094 R6)", () => { it("defaults: MENTION/ASSIGNMENT instant, the rest digest", () => { expect(DEFAULT_NOTIFICATION_PREFS.MENTION).toBe("instant"); expect(DEFAULT_NOTIFICATION_PREFS.ASSIGNMENT).toBe("instant"); expect(DEFAULT_NOTIFICATION_PREFS.COMMENT).toBe("digest"); expect(DEFAULT_NOTIFICATION_PREFS.DATABASE_CHANGE).toBe("digest"); expect(DEFAULT_NOTIFICATION_PREFS.PAGE_SHARED).toBe("digest"); expect(DEFAULT_NOTIFICATION_PREFS.AUTOMATION).toBe("digest"); expect(DEFAULT_NOTIFICATION_PREFS.INTEGRATION).toBe("digest"); expect(DEFAULT_NOTIFICATION_PREFS.TASK_ASSIGNED).toBe("digest"); }); it("effectivePrefMode falls back to the default for an absent type", () => { expect(effectivePrefMode("MENTION", {})).toBe("instant"); expect(effectivePrefMode("COMMENT", {})).toBe("digest"); }); it("an explicit pref overrides the default", () => { expect(effectivePrefMode("COMMENT", { COMMENT: "instant" })).toBe("instant"); expect(effectivePrefMode("MENTION", { MENTION: "off" })).toBe("off"); }); it("prefSendsInstant: instant | all → true; digest → false", () => { expect(prefSendsInstant("MENTION", {})).toBe(true); // default instant expect(prefSendsInstant("COMMENT", {})).toBe(false); // default digest expect(prefSendsInstant("COMMENT", { COMMENT: "all" })).toBe(true); // "all" = send-now expect(prefSendsInstant("COMMENT", { COMMENT: "digest" })).toBe(false); }); it("prefIsOff: only 'off' suppresses", () => { expect(prefIsOff("MENTION", { MENTION: "off" })).toBe(true); expect(prefIsOff("MENTION", {})).toBe(false); }); it("NotificationPrefsSchema accepts a partial map + rejects a bad mode", () => { const ok = NotificationPrefsSchema.safeParse({ MENTION: "off", COMMENT: "instant" }); expect(ok.success).toBe(true); if (ok.success) expect(ok.data).toEqual({ MENTION: "off", COMMENT: "instant" }); const empty = NotificationPrefsSchema.safeParse({}); expect(empty.success).toBe(true); if (empty.success) expect(empty.data).toEqual({}); const bad = NotificationPrefsSchema.safeParse({ MENTION: "sometimes" }); expect(bad.success).toBe(false); }); });