import { describe, expect, it } from "vitest"; import type { ChatMemberChannelDetail, ChatMemberChannelRecord } from "@hivegrid/chat"; import type { ChatRealtimeSystemMessage } from "@hivegrid/realtime"; import { buildMemberChatChannelDetailVM, buildMemberChatChannelListVM, type ChatViewModelLabels } from "./chatViewModel.server"; const labels: ChatViewModelLabels = { directMessage: "Direktnachricht", unnamedGroup: "Unbenannte Gruppe", unnamedProject: "Projektchat", unknownUser: "Unbekannte Person", unknownMention: "Unbekannte Erwähnung", }; const channel = (overrides: Partial = {}): ChatMemberChannelRecord => ({ id: "chat_1", kind: "dm", name: null, description: null, projektId: null, isPrivate: true, archivedAtIso: null, lastMessageAtIso: "2026-07-15T10:00:00.000Z", eventStreamId: "stream_1", eventCursor: "stream_1:8", member: { roleInChat: "member", notificationPreference: "all_messages", lastReadMessageId: null, lastReadAtIso: null, isPinned: false, canWrite: true, }, memberCount: 2, participants: [ { userId: "user_me", displayName: "Raffael Sheikh", avatarUrl: "/avatars/user_me?v=1" }, { userId: "user_other", displayName: "Ada Lovelace", avatarUrl: null }, ], unreadCount: 1, hasUnreadMention: true, lastMessage: { id: "message_1", clientMessageId: "client_1", authorUserId: "user_other", body: "Hallo Raffael", bodyFormat: "markdown", parentMessageId: null, mentions: [{ type: "user", id: "user_me", label: null }], attachmentIds: [], linkPreviews: [], createdAt: "2026-07-15T10:00:00.000Z", updatedAt: "2026-07-15T10:00:00.000Z", editedAt: null, deletedAt: null, }, ...overrides, }); const systemMessage = (overrides: Partial = {}): ChatRealtimeSystemMessage => ({ id: "system_message_1", clientMessageId: "system:project-created:project_1", authorUserId: null, body: "stored fallback copy", bodyFormat: "system", systemEvent: { kind: "project_created", metadata: { projectName: "Wärmepumpe Müller" } }, parentMessageId: null, mentions: [], attachmentIds: [], linkPreviews: [], createdAt: "2026-07-15T10:00:00.000Z", updatedAt: "2026-07-15T10:00:00.000Z", editedAt: null, deletedAt: null, ...overrides, }); describe("chatViewModel.server", () => { it("maps the repository page without applying query, view, or archive filters a second time", () => { const vm = buildMemberChatChannelListVM({ channels: [channel(), channel({ id: "chat_archived", archivedAtIso: "2026-07-14T10:00:00.000Z", unreadCount: 3 })], tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels", "chat.write", "chat.create_dm"], filter: { query: "ada", view: "mentions", showArchived: false }, nextCursor: "next_cursor", labels, }); expect(vm.status).toBe("ready"); if (vm.status !== "ready") return; expect(vm.data.rows).toHaveLength(2); expect(vm.data.rows[0]).toMatchObject({ id: "chat_1", title: "Ada Lovelace", detailHref: "/demo/chat/chat_1", unreadCount: 1, hasMention: true, avatar: { fallbackLabel: "AL" }, }); expect(vm.data.totals).toEqual({ unread: 4, mentions: 2 }); expect(vm.data.nextCursor).toBe("next_cursor"); expect(vm.data.permissions).toMatchObject({ canWrite: true, canCreateDm: true, canCreateGroup: false, canSearch: true }); expect(vm.data.hrefs).toMatchObject({ newChannel: "/demo/chat/new?kind=group", newDm: "/demo/chat/new?kind=dm", }); }); it("renders a bounded plain-text channel snippet from long markdown", () => { const markdown = `**Wichtig** [Plan](https://example.com) ${"x".repeat(300)}`; const vm = buildMemberChatChannelListVM({ channels: [channel({ lastMessage: { ...channel().lastMessage!, body: markdown } })], tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels"], filter: { query: "", view: "all", showArchived: false }, nextCursor: null, labels, }); expect(vm.status).toBe("ready"); if (vm.status !== "ready") return; expect(vm.data.rows[0]?.lastMessage?.snippet).toMatch(/^Wichtig Plan x+…$/); expect(vm.data.rows[0]?.lastMessage?.snippet).toHaveLength(160); }); it("maps discoverable public groups to join-only rows without exposing a detail route", () => { const vm = buildMemberChatChannelListVM({ channels: [], discoverChannels: [{ id: "chat_public", name: "Montage Nord", description: "Offener Austausch", memberCount: 7 }], tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels"], filter: { query: "", view: "all", showArchived: false }, nextCursor: null, labels, }); expect(vm.status).toBe("ready"); if (vm.status !== "ready") return; expect(vm.data.permissions.canDiscover).toBe(true); expect(vm.data.discoverRows).toEqual([ { id: "chat_public", title: "Montage Nord", description: "Offener Austausch", avatar: { imageUrl: null, fallbackLabel: "MN" }, memberCount: 7, isJoined: false, canJoin: true, joinHref: "/demo/chat", detailHref: null, }, ]); }); it("maps the latest system event to an i18n preview without a fake author", () => { const vm = buildMemberChatChannelListVM({ channels: [channel({ lastMessage: systemMessage() })], tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels"], filter: { query: "", view: "all", showArchived: false }, nextCursor: null, labels, }); expect(vm.status).toBe("ready"); if (vm.status !== "ready") return; expect(vm.data.rows[0]?.lastMessage).toMatchObject({ authorLabel: null, snippet: "", systemEvent: { kind: "project_created", labelKey: "chat.systemEvent.projectCreated", values: { project: "Wärmepumpe Müller" }, }, }); }); it("maps persisted messages, resolved mentions, reads and the enabled thread/reaction actions", () => { const detail: ChatMemberChannelDetail = { channel: channel(), messages: [channel().lastMessage!], readReceipts: [{ messageId: "message_1", userId: "user_me", readAtIso: "2026-07-15T10:01:00.000Z" }], attachments: [ { messageId: "message_1", fileId: "file_1", displayName: "camera.jpg", mimeType: "image/jpeg", sizeBytes: 1024, linkRole: "image", previewStatus: "ready", scanStatus: "clean", }, ], hasOlderMessages: true, olderMessageCursor: "opaque_cursor", eventCursor: "stream_1:8", }; const vm = buildMemberChatChannelDetailVM({ detail, tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels", "chat.write", "files.write", "tasks.write"], featureAvailability: { files: true, tasks: true }, labels, }); expect(vm.status).toBe("ready"); if (vm.status !== "ready") return; expect(vm.data.header).toMatchObject({ title: "Ada Lovelace", kind: "dm", pinnedHref: null }); expect(vm.data.history).toEqual({ hasOlder: true, olderCursor: "opaque_cursor" }); expect(vm.data.messages[0]).toMatchObject({ source: "persisted", deliveryState: "sent", author: { displayName: "Ada Lovelace", isCurrentUser: false }, mentions: [{ kind: "user", id: "user_me", label: "Raffael Sheikh", href: null }], readAggregate: { readCount: 1, recipientCount: 1, readerUserIds: ["user_me"], readerLabels: ["Raffael Sheikh"] }, attachments: [{ fileId: "file_1", kind: "image", previewHref: "/demo/media/file_1/preview", scanStatus: "clean" }], actions: { canReact: true, canReply: true, canEdit: false, canDelete: false, canPin: false, canMarkAsTask: true }, }); expect(vm.data.composer).toMatchObject({ enabled: true, canSend: true, canMention: true, canAttachFiles: true, canRecordVoice: true, canMarkAsTask: true, }); }); it("projects reactions, one-level thread aggregates, and pins for non-DM channels", () => { const group = channel({ kind: "group", name: "Montage Nord", isPrivate: false, member: { ...channel().member, roleInChat: "admin" }, }); const vm = buildMemberChatChannelDetailVM({ detail: { channel: group, messages: [group.lastMessage!], readReceipts: [], attachments: [], messageFeatures: [ { messageId: "message_1", reactions: [ { id: "reaction_1", messageId: "message_1", userId: "user_me", emoji: "👍", reactedAtIso: "2026-07-15T10:02:00.000Z" }, { id: "reaction_2", messageId: "message_1", userId: "user_other", emoji: "👍", reactedAtIso: "2026-07-15T10:03:00.000Z" }, ], thread: { id: "message_1", parentMessageId: "message_1", replyCount: 2, lastReplyAtIso: "2026-07-15T10:04:00.000Z", participantUserIds: ["user_me", "user_other"], }, pin: { messageId: "message_1", pinnedByUserId: "user_me", pinnedAtIso: "2026-07-15T10:05:00.000Z", note: null }, }, ], hasOlderMessages: false, olderMessageCursor: null, eventCursor: "stream_1:8", }, tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels", "chat.write"], labels, }); expect(vm.status).toBe("ready"); if (vm.status !== "ready") return; expect(vm.data.header.pinnedHref).toBe("/demo/chat/chat_1/pinned"); expect(vm.data.messages[0]).toMatchObject({ reactions: [{ emoji: "👍", count: 2, reactedByViewer: true, reactorLabels: ["Raffael Sheikh", "Ada Lovelace"] }], thread: { replyCount: 2, href: "/demo/chat/chat_1/thread/message_1", isFollowing: true }, pin: { pinnedByLabel: "Raffael Sheikh" }, actions: { canReact: true, canReply: true, canPin: true }, }); expect(vm.data.pinnedMessages).toHaveLength(1); }); it("keeps archived channels read-only", () => { const archived = channel({ archivedAtIso: "2026-07-15T09:00:00.000Z" }); const vm = buildMemberChatChannelDetailVM({ detail: { channel: archived, messages: [], readReceipts: [], attachments: [], hasOlderMessages: false, olderMessageCursor: null, eventCursor: "stream_1:0", }, tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels", "chat.write"], labels, }); expect(vm.status).toBe("ready"); if (vm.status !== "ready") return; expect(vm.data.composer).toMatchObject({ enabled: false, canSend: false, disabledReasonKey: "chat.composer.archived" }); }); it("derives edit and delete actions from the deterministic 24-hour window and fresh admin capabilities", () => { const ownMessage = { ...channel().lastMessage!, authorUserId: "user_me" }; const detail = (memberRole: "admin" | "member", message = ownMessage): ChatMemberChannelDetail => ({ channel: channel({ kind: "group", name: "Team", member: { ...channel().member, roleInChat: memberRole }, lastMessage: message }), messages: [message], readReceipts: [], attachments: [], hasOlderMessages: false, olderMessageCursor: null, eventCursor: "stream_1:8", }); const freshOwn = buildMemberChatChannelDetailVM({ detail: detail("member"), tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels", "chat.write"], nowIso: "2026-07-16T09:00:00.000Z", labels, }); const expiredOwn = buildMemberChatChannelDetailVM({ detail: detail("member"), tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels", "chat.write"], nowIso: "2026-07-17T11:00:00.000Z", labels, }); const channelAdmin = buildMemberChatChannelDetailVM({ detail: detail("admin", channel().lastMessage!), tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels", "chat.write"], nowIso: "2026-07-17T11:00:00.000Z", labels, }); const globalAdmin = buildMemberChatChannelDetailVM({ detail: detail("member", channel().lastMessage!), tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels", "chat.write", "chat.admin"], nowIso: "2026-07-17T11:00:00.000Z", labels, }); expect(freshOwn.status === "ready" && freshOwn.data.messages[0]?.actions).toMatchObject({ canEdit: true, canDelete: true }); expect(expiredOwn.status === "ready" && expiredOwn.data.messages[0]?.actions).toMatchObject({ canEdit: false, canDelete: false }); expect(channelAdmin.status === "ready" && channelAdmin.data.messages[0]?.actions).toMatchObject({ canEdit: false, canDelete: true }); expect(globalAdmin.status === "ready" && globalAdmin.data.messages[0]?.actions).toMatchObject({ canEdit: true, canDelete: true }); }); it("maps actorless system messages fail-soft without author, receipts, or message actions", () => { const vm = buildMemberChatChannelDetailVM({ detail: { channel: channel({ lastMessage: systemMessage() }), messages: [ systemMessage(), systemMessage({ id: "system_message_2", clientMessageId: "system:future", systemEvent: { kind: "channel_renamed", metadata: { internalId: "secret" } }, }), ], readReceipts: [{ messageId: "system_message_1", userId: "user_me", readAtIso: "2026-07-15T10:01:00.000Z" }], attachments: [], hasOlderMessages: false, olderMessageCursor: null, eventCursor: "stream_1:8", }, tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels", "chat.write"], labels, }); expect(vm.status).toBe("ready"); if (vm.status !== "ready") return; expect(vm.data.messages[0]).toMatchObject({ author: null, bodyMarkdown: null, bodyFormat: "system", readAggregate: null, systemEvent: { kind: "project_created", labelKey: "chat.systemEvent.projectCreated", values: { project: "Wärmepumpe Müller" }, }, actions: { canReact: false, canReply: false, canEdit: false, canDelete: false, canPin: false, canMarkAsTask: false }, }); expect(vm.data.messages[1]?.systemEvent).toEqual({ kind: "channel_renamed", labelKey: "chat.systemEvent.generic", values: {}, }); }); it("keeps deleted message projections free of stale mention and preview metadata", () => { const deletedMessage = { ...channel().lastMessage!, body: "deleted secret", deletedAt: "2026-07-15T10:02:00.000Z", mentions: [{ type: "user" as const, id: "user_me", label: null }], linkPreviews: [ { url: "https://example.com/secret", title: "Secret", description: null, imageUrl: null, siteName: "Example", }, ], }; const vm = buildMemberChatChannelDetailVM({ detail: { channel: channel(), messages: [deletedMessage], readReceipts: [], attachments: [ { messageId: deletedMessage.id, fileId: "file_secret", displayName: "secret.pdf", mimeType: "application/pdf", sizeBytes: 10, linkRole: "file", previewStatus: "not_supported", scanStatus: "clean", }, ], hasOlderMessages: false, olderMessageCursor: null, eventCursor: "stream_1:1", }, tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels", "chat.write"], labels, }); expect(vm.status).toBe("ready"); if (vm.status !== "ready") return; expect(vm.data.messages[0]).toMatchObject({ bodyMarkdown: null, mentions: [], linkPreviews: [], attachments: [], deletedAtIso: "2026-07-15T10:02:00.000Z", }); }); it("maps skipped attachment scans fail-closed instead of exposing a download", () => { const persisted = channel().lastMessage!; const vm = buildMemberChatChannelDetailVM({ detail: { channel: channel(), messages: [persisted], readReceipts: [], attachments: [ { messageId: persisted.id, fileId: "file_skipped", displayName: "unchecked.pdf", mimeType: "application/pdf", sizeBytes: 20, linkRole: "file", previewStatus: "not_supported", scanStatus: "skipped", }, ], hasOlderMessages: false, olderMessageCursor: null, eventCursor: "stream_1:1", }, tenantSlug: "demo", currentUserId: "user_me", permissions: ["chat.read_own_channels"], labels, }); expect(vm.status === "ready" ? vm.data.messages[0]?.attachments[0]?.scanStatus : null).toBe("blocked"); }); });