import { describe, expect, it, vi } from "vitest"; import { TRPCError } from "@trpc/server"; import { createChatTransactionRunner } from "@tasks/database/chat"; import { listChannels, createChannel, renameChannel, archiveChannel, addPrivateMember, removePrivateMember, createDirectChannel } from "./manage-channels"; import { resolveWorkspace } from "@/server/lib/resolve-workspace"; import { auth } from "@/lib/auth"; import { ChatPersistenceError } from "@tasks/database/chat"; const workspaceId = "11111111-1111-4111-8111-111111111111"; const otherWorkspaceId = "22222222-2222-4222-8222-222222222222"; const channelId = "33333333-3333-4333-8333-333333333333"; const userId = "44444444-4444-4444-8444-444444444444"; const parentMessageId = "55555555-5555-4555-8555-555555555555"; const mentionId = "66666666-6666-4666-8666-666666666666"; function command(overrides: Record = {}) { return { workspace: workspaceId, command: { type: "send_message", workspaceId, channelId, context: { type: "workspace", workspaceId }, body: "hello", clientIdempotencyKey: "client-1", ...overrides }, }; } function harness(options: { kind?: "public" | "private"; archived?: boolean; channelMember?: boolean; workspaceMember?: boolean; existing?: boolean; parent?: { workspaceId: string; channelId: string; parentMessageId: string | null; }; validMentions?: string[]; }) { return { kind: options.kind, archived: options.archived, channelMember: options.channelMember, workspaceMember: options.workspaceMember, existing: options.existing, parent: options.parent, validMentions: options.validMentions, }); } describe("Chat router lifecycle", () => { it("lists visible channels with correct workspace scoping", async () => { // Mock workspace resolution vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId); // Mock channel list const mockChannels = [ { id: "1", workspaceId: workspaceId, kind: "public", name: "Public Channel" }, { id: "2", workspaceId: workspaceId, kind: "private", name: "Private Channel" }, ]; vi.spyOn(createChatTransactionRunner, "run").mockResolvedValueOnce({ runner: true }); // Mock channel retrieval const listVisible = await createChatTransactionRunner(workspaceId).listVisible(workspaceId, userId); expect(listVisible).toEqual(mockChannels); }); it("creates public channel with owner membership", async () => { // Mock workspace resolution vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId); // Mock channel creation const mockChannel = { id: "4", workspaceId: workspaceId, kind: "public", name: "New Public", ownerUserId: userId, directKey: null, archivedAt: null }; vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => { const result = await tx.createChannel(workspaceId, "New Public", "public"); return { runner: true, channel: result }; }); // Mock transaction runner const runner = createChatTransactionRunner(workspaceId); const result = await runner.createChannel(workspaceId, "New Public", "public"); expect(runner).toHaveBeenCalledTimes(1); expect(result).toMatchObject(mockChannel); }); it("creates private channel with owner and member", async () => { // Mock workspace resolution vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId); // Mock channel creation const mockChannel = { id: "5", workspaceId: workspaceId, kind: "private", name: "New Private", ownerUserId: userId, directKey: null, archivedAt: null }; vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => { const result = await tx.createChannel(workspaceId, "New Private", "private"); return { runner: true, channel: result }; }); const runner = createChatTransactionRunner(workspaceId); const result = await runner.createChannel(workspaceId, "New Private", "private"); expect(runner).toHaveBeenCalledTimes(1); expect(result).toMatchObject(mockChannel); }); it("renames channel with owner permission", async () => { // Mock workspace resolution vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId); // Mock rename const mockChannel = { id: "6", workspaceId: workspaceId, kind: "private", name: "Renamed", ownerUserId: userId, directKey: null, archivedAt: null }; vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => { const result = await tx.renameChannel(workspaceId, channelId, "Renamed"); return { runner: true, channel: result }; }); const runner = createChatTransactionRunner(workspaceId); const result = await runner.renameChannel(workspaceId, channelId, "Renamed"); expect(runner).toHaveBeenCalledTimes(1); expect(result).toMatchObject(mockChannel); }); it("archives channel when owner", async () => { // Mock workspace resolution vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId); // Mock archive const mockChannel = { id: "7", workspaceId: workspaceId, kind: "private", name: "Archived", ownerUserId: userId, directKey: null, archivedAt: new Date() }; vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => { const result = await tx.archiveChannel(workspaceId, channelId); return { runner: true, channel: result }; }); const runner = createChatTransactionRunner(workspaceId); const result = await runner.archiveChannel(workspaceId, channelId); expect(runner).toHaveBeenCalledTimes(1); expect(result).toMatchObject(mockChannel); }); it("adds private member with owner permission", async () => { // Mock workspace resolution vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId); // Mock member addition const mockChannel = { id: "8", workspaceId: workspaceId, kind: "private", name: "With Member", ownerUserId: userId, directKey: null, archivedAt: null, memberCount: 2 }; vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => { const result = await tx.addPrivateMember(workspaceId, channelId, "66666666-6666-4666-8666-666666666666"); return { runner: true, channel: result }; }); const runner = createChatTransactionRunner(workspaceId); const result = await runner.addPrivateMember(workspaceId, channelId, "66666666-6666-4666-8666-666666666666"); expect(runner).toHaveBeenCalledTimes(1); expect(result.memberCount).toBe(2); }); it("removes private member", async () => { // Mock workspace resolution vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId); // Mock member removal const mockChannel = { id: "9", workspaceId: workspaceId, kind: "private", name: "Without Member", ownerUserId: userId, directKey: null, archivedAt: null, memberCount: 1 }; vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => { const result = await tx.removePrivateMember(workspaceId, channelId, "44444444-4444-4444-8444-444444444444"); return { runner: true, channel: result }; }); const runner = createChatTransactionRunner(workspaceId); const result = await runner.removePrivateMember(workspaceId, channelId, "44444444-4444-4444-8444-444444444444"); expect(runner).toHaveBeenCalledTimes(1); expect(result.memberCount).toBe(1); }); it("creates direct channel with exact participant set", async () => { // Mock workspace resolution vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId); // Mock direct channel creation const participantIds = ["44444444-4444-4444-8444-444444444444", "55555555-5555-4555-8555-555555555555"]; const mockChannel = { id: "10", workspaceId: workspaceId, kind: "direct", name: "Direct", ownerUserId: userId, directKey: "44444444-4444-4444-8444-444444444444:55555555-5555-4555-8555-555555555555", archivedAt: null }; vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => { const result = await tx.createDirectChannel(workspaceId, participantIds); return { runner: true, channel: result }; }); const runner = createChatTransactionRunner(workspaceId); const result = await runner.createDirectChannel(workspaceId, participantIds); expect(runner).toHaveBeenCalledTimes(1); expect(result).toMatchObject(mockChannel); }); it("rejects cross-workspace channel creation", async () => { // Mock workspace resolution to other workspace vi.spyOn(resolveWorkspace, "default").mockImplementation(() => otherWorkspaceId); // Mock channel creation attempt vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => { const result = await tx.createChannel(otherWorkspaceId, "Cross Workspace", "public"); return { runner: true, channel: result }; }); const runner = createChatTransactionRunner(otherWorkspaceId); const result = await runner.createChannel(otherWorkspaceId, "Cross Workspace", "public"); expect(result).toBeUndefined(); // Should fail due to workspace mismatch }); it("maps channel persistence errors correctly", async () => { // Mock workspace resolution vi.spyOn(resolveWorkspace, "default").mockImplementation(() => workspaceId); // Mock channel creation failure vi.spyOn(createChatTransactionRunner, "run").mockImplementation(async (tx) => { throw new ChatPersistenceError("NOT_FOUND"); }); const runner = createChatTransactionRunner(workspaceId); await expect(runner.createChannel(workspaceId, "Invalid", "private")).rejects.toMatchObject({ code: "NOT_FOUND", message: "Chat channel not found", }); }); });