import { afterAll, beforeAll, beforeEach, describe, expect, it, vi, } from "vitest"; import { closeDatabase, getDatabase, initializeDatabase, } from "../../src/database/drizzle"; import { captureMessage } from "../../src/moderation/messageCapture"; import type { ModerationBroadcaster } from "../../src/moderation/types"; const queueMessageAnalysis = vi.fn(); type TestMessage = Parameters[0]; type ModerationTestGlobal = typeof globalThis & { moderationBroadcaster?: Partial; }; interface TestDatabase { run(sql: string): void; } function getTestDatabase(): TestDatabase { return getDatabase() as unknown as TestDatabase; } vi.mock("../../src/moderation/aiAnalyzer", () => ({ queueMessageAnalysis: (id: string) => queueMessageAnalysis(id), })); function createMessage(id = "message-1"): TestMessage { return { id, guildId: "guild-1", channelId: "channel-1", author: { id: "user-1", username: "alice", bot: false, avatarURL: () => null, }, content: "hello", cleanContent: "hello", createdTimestamp: 1_700_000_000_000, attachments: new Map(), stickers: new Map(), embeds: [], member: null, reference: null, channel: { id: "channel-1", name: "general", isThread: () => false, }, } as unknown as TestMessage; } async function createTables() { const db = getTestDatabase(); db.run(` CREATE TABLE IF NOT EXISTS "messages" ( "id" text PRIMARY KEY NOT NULL, "guild_id" text NOT NULL, "channel_id" text NOT NULL, "thread_id" text, "user_id" text NOT NULL, "username" text NOT NULL, "avatar_url" text, "content" text NOT NULL, "edited_content" text, "created_at" integer NOT NULL, "edited_at" integer, "deleted_at" integer, "type" text DEFAULT 'text' NOT NULL, "metadata" text, "ai_status" text DEFAULT 'pending' NOT NULL, "ai_moderation_flags" text, "ai_moderation_score" real, "ai_moderation_raw" text, "ai_analysis" text, "ai_analyzed_at" integer, "ai_error" text ) `); db.run(` CREATE TABLE IF NOT EXISTS "attachments" ( "id" text PRIMARY KEY NOT NULL, "message_id" text NOT NULL, "guild_id" text NOT NULL, "channel_id" text NOT NULL, "thread_id" text, "user_id" text NOT NULL, "filename" text NOT NULL, "size" integer NOT NULL, "type" text NOT NULL, "discord_url" text NOT NULL, "uploaded_url" text, "upload_status" text DEFAULT 'pending' NOT NULL, "upload_error" text, "created_at" integer NOT NULL, "uploaded_at" integer ) `); } describe("captureMessage", () => { beforeAll(async () => { await initializeDatabase(); await createTables(); }); beforeEach(async () => { queueMessageAnalysis.mockClear(); const db = getTestDatabase(); db.run(`DELETE FROM "attachments"`); db.run(`DELETE FROM "messages"`); delete (globalThis as ModerationTestGlobal).moderationBroadcaster; }); afterAll(async () => { await closeDatabase(); }); it("does not requeue or rebroadcast a duplicate captured message", async () => { const message = createMessage(); const messageCreated = vi.fn(); (globalThis as ModerationTestGlobal).moderationBroadcaster = { messageCreated, }; await captureMessage(message, "text"); await captureMessage(message, "text"); expect(queueMessageAnalysis).toHaveBeenCalledTimes(1); expect(messageCreated).toHaveBeenCalledTimes(1); }); it("does not queue or broadcast backlog captures one message at a time", async () => { const message = createMessage("backlog-message-1"); const messageCreated = vi.fn(); (globalThis as ModerationTestGlobal).moderationBroadcaster = { messageCreated, }; await captureMessage(message, "text", { source: "backlog" }); expect(queueMessageAnalysis).not.toHaveBeenCalled(); expect(messageCreated).not.toHaveBeenCalled(); }); });