Files
dc-recorder/tests/moderation/messageCapture.test.ts

157 lines
4.0 KiB
TypeScript
Raw Normal View History

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<typeof captureMessage>[0];
type ModerationTestGlobal = typeof globalThis & {
moderationBroadcaster?: Partial<ModerationBroadcaster>;
};
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();
});
});