1
6import { describe, expect, it } from "vitest";
7
8import type { DomainEvent } from "@factory/shared";
9import { commentEventFrameFor, COMMENT_PREVIEW_MAX } from "../src/comments.js";
10import { notificationPushFor, NOTIFICATION_CREATED_EVENT } from "../src/notifications.js";
11
12const evt = (name: string, payload: Record<string, unknown>): DomainEvent =>
13 ({
14 id: "evt_x",
15 name: name as DomainEvent["name"],
16 payload,
17 ts: "2026-09-27T00:00:00.000Z",
18 }) as DomainEvent;
19
20describe("commentEventFrameFor (TASK-0094 R1)", () => {
21 it("comment.created (no replyId) → kind 'created' + a body preview", () => {
22 const f = commentEventFrameFor(
23 evt("comment.created", {
24 commentId: "c1",
25 pageId: "pg_1",
26 actorId: "u1",
27 body: "hello world",
28 }),
29 );
30 expect(f).toEqual({
31 type: "comment.event",
32 kind: "created",
33 commentId: "c1",
34 pageId: "pg_1",
35 actorId: "u1",
36 preview: "hello world",
37 });
38 });
39
40 it("comment.created WITH a replyId → kind 'replied'", () => {
41 const f = commentEventFrameFor(
42 evt("comment.created", {
43 commentId: "c1",
44 replyId: "r1",
45 pageId: "pg_1",
46 actorId: "u2",
47 body: "a reply",
48 }),
49 );
50 expect(f?.kind).toBe("replied");
51 expect(f?.preview).toBe("a reply");
52 });
53
54 it("truncates the preview to the max length", () => {
55 const long = "x".repeat(COMMENT_PREVIEW_MAX + 50);
56 const f = commentEventFrameFor(
57 evt("comment.created", { commentId: "c", pageId: "p", actorId: "u", body: long }),
58 );
59 expect(f?.preview).toHaveLength(COMMENT_PREVIEW_MAX);
60 });
61
62 it("comment.resolved / reopened / reacted / deleted → the matching kind, empty preview", () => {
63 for (const [name, kind] of [
64 ["comment.resolved", "resolved"],
65 ["comment.reopened", "reopened"],
66 ["comment.reaction", "reacted"],
67 ["comment.deleted", "deleted"],
68 ] as const) {
69 const f = commentEventFrameFor(evt(name, { commentId: "c", pageId: "p", actorId: "u" }));
70 expect(f?.kind).toBe(kind);
71 expect(f?.preview).toBe("");
72 }
73 });
74
75 it("an event without a pageId → null (no room to fan out to)", () => {
76 expect(
77 commentEventFrameFor(evt("comment.created", { commentId: "c", actorId: "u", body: "b" })),
78 ).toBeNull();
79 });
80
81 it("a malformed payload → null (never a crash)", () => {
82 expect(commentEventFrameFor(evt("comment.created", { pageId: "p", body: "b" }))).toBeNull();
83 expect(commentEventFrameFor(evt("comment.created", {}))).toBeNull();
84 });
85
86 it("an unrelated event → null (the mapper skips it)", () => {
87 expect(commentEventFrameFor(evt("page.updated", { pageId: "p" }))).toBeNull();
88 expect(
89 commentEventFrameFor(evt("notification.created", { notificationId: "n", userId: "u" })),
90 ).toBeNull();
91 });
92
93 it("a reaction event carries the emoji/active in the payload (validated, not in the frame)", () => {
94 const f = commentEventFrameFor(
95 evt("comment.reaction", {
96 commentId: "c",
97 pageId: "p",
98 actorId: "u",
99 emoji: "👍",
100 active: true,
101 }),
102 );
103 expect(f?.kind).toBe("reacted");
104
105 expect("emoji" in (f as object)).toBe(false);
106 });
107});
108
109describe("notificationPushFor (TASK-0094 R7)", () => {
110 const base = {
111 notificationId: "n1",
112 userId: "u1",
113 type: "MENTION",
114 title: "You were mentioned",
115 deepLink: { type: "comment", id: "c1" },
116 };
117
118 it("notification.created → the push frame + the RECIPIENT as target", () => {
119 const t = notificationPushFor(evt("notification.created", { ...base, actorName: "Ana" }));
120 expect(t?.userId).toBe("u1");
121 expect(t?.frame).toEqual({
122 type: "notification.push",
123 notificationId: "n1",
124 kind: "mention",
125 title: "You were mentioned",
126 deepLink: { type: "comment", id: "c1" },
127 actorName: "Ana",
128 });
129 });
130
131 it("the kind is the policy table's snake_case type (not a raw enum)", () => {
132 expect(
133 notificationPushFor(evt("notification.created", { ...base, type: "PAGE_SHARED" }))?.frame
134 .kind,
135 ).toBe("page_shared");
136 expect(
137 notificationPushFor(evt("notification.created", { ...base, type: "COMMENT" }))?.frame.kind,
138 ).toBe("comment");
139 expect(
140 notificationPushFor(evt("notification.created", { ...base, type: "ASSIGNMENT" }))?.frame.kind,
141 ).toBe("assignment");
142 });
143
144 it("a non-notification event → null", () => {
145 expect(notificationPushFor(evt("comment.created", { commentId: "c", pageId: "p" }))).toBeNull();
146 });
147
148 it("an unknown type still produces a frame (conservative kind, no crash)", () => {
149 const t = notificationPushFor(
150 evt("notification.created", { ...base, type: "SOME_FUTURE_TYPE" }),
151 );
152 expect(t?.frame.kind).toBe("some_future_type");
153 });
154
155 it("a malformed payload → null (no notificationId / userId / deepLink)", () => {
156 expect(
157 notificationPushFor(
158 evt("notification.created", { userId: "u1", type: "MENTION", title: "t" }),
159 ),
160 ).toBeNull();
161 expect(
162 notificationPushFor(
163 evt("notification.created", {
164 notificationId: "n",
165 type: "MENTION",
166 title: "t",
167 deepLink: { type: "x", id: "y" },
168 }),
169 ),
170 ).toBeNull();
171 expect(
172 notificationPushFor(
173 evt("notification.created", {
174 notificationId: "n",
175 userId: "u",
176 type: "MENTION",
177 title: "t",
178 }),
179 ),
180 ).toBeNull();
181 });
182
183 it("the event name constant matches the frame builder's filter", () => {
184 expect(NOTIFICATION_CREATED_EVENT).toBe("notification.created");
185 });
186});
187