157 lines
4.0 KiB
TypeScript
157 lines
4.0 KiB
TypeScript
|
|
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();
|
||
|
|
});
|
||
|
|
});
|