From 0c54ac4ba8e9e335d0ea1df49d12af4c2124b88c Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Thu, 14 May 2026 18:54:13 +0700 Subject: [PATCH] test: cover message query pagination --- tests/moderation/messageStoreQueries.test.ts | 482 ++++++++++++++++++- 1 file changed, 480 insertions(+), 2 deletions(-) diff --git a/tests/moderation/messageStoreQueries.test.ts b/tests/moderation/messageStoreQueries.test.ts index eb6e718..e6b1029 100644 --- a/tests/moderation/messageStoreQueries.test.ts +++ b/tests/moderation/messageStoreQueries.test.ts @@ -1,5 +1,17 @@ -import { describe, expect, it } from "vitest"; -import { decodeCursor, encodeCursor } from "../../src/moderation/messageStore"; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { + decodeCursor, + encodeCursor, + insertMessage, + listMessages, + listReviewMessages, +} from "../../src/moderation/messageStore"; +import { getDatabase, initializeDatabase, closeDatabase } from "../../src/database/drizzle"; +import { messagesTable } from "../../src/database/schema"; +import { createChildLogger } from "../../src/logger"; +import type { MessageRecord } from "../../src/moderation/types"; + +const logger = createChildLogger("messageStoreQueries.test"); describe("message cursor helpers", () => { it("round-trips created_at and id", () => { @@ -11,3 +23,469 @@ describe("message cursor helpers", () => { expect(decodeCursor("not-base64-json")).toBeNull(); }); }); + +describe("listMessages integration tests", () => { + beforeAll(async () => { + // Initialize database once for all tests + await initializeDatabase(); + }); + + afterAll(async () => { + try { + await closeDatabase(); + } catch (error) { + logger.error( + { error: error instanceof Error ? error.message : String(error) }, + "Error closing database in afterAll", + ); + } + }); + + beforeEach(async () => { + // Clear messages table before each test + try { + const db = getDatabase(); + await db.delete(messagesTable); + } catch (error) { + logger.debug("Could not clear messages table", { error }); + } + }); + + const createTestMessage = ( + overrides: Partial = {}, + ): MessageRecord => ({ + id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`, + guild_id: "guild-123", + channel_id: "channel-456", + thread_id: null, + user_id: "user-789", + username: "testuser", + avatar_url: null, + content: "Test message", + edited_content: null, + created_at: Date.now(), + edited_at: null, + deleted_at: null, + type: "text", + metadata: null, + ai_status: "pending", + ...overrides, + }); + + it("returns messages in newest-first order", async () => { + const now = Date.now(); + const msg1 = createTestMessage({ + id: "msg-1", + created_at: now - 3000, + content: "oldest", + }); + const msg2 = createTestMessage({ + id: "msg-2", + created_at: now - 2000, + content: "middle", + }); + const msg3 = createTestMessage({ + id: "msg-3", + created_at: now - 1000, + content: "newest", + }); + + await insertMessage(msg1); + await insertMessage(msg2); + await insertMessage(msg3); + + const result = await listMessages({ + channelId: "channel-456", + limit: 10, + }); + + expect(result.data).toHaveLength(3); + expect(result.data[0].id).toBe("msg-3"); + expect(result.data[1].id).toBe("msg-2"); + expect(result.data[2].id).toBe("msg-1"); + }); + + it("returns nextCursor when more results exist than limit", async () => { + const now = Date.now(); + const channelId = `channel-${Math.random().toString(36).slice(2)}`; + + // Insert 5 messages + for (let i = 0; i < 5; i++) { + await insertMessage( + createTestMessage({ + id: `msg-limit-${i}`, + channel_id: channelId, + created_at: now - i * 1000, + }), + ); + } + + const result = await listMessages({ + channelId, + limit: 3, + }); + + expect(result.data).toHaveLength(3); + expect(result.nextCursor).not.toBeNull(); + }); + + it("returns null nextCursor when all results fit within limit", async () => { + const now = Date.now(); + const channelId = `channel-${Math.random().toString(36).slice(2)}`; + + // Insert 2 messages + for (let i = 0; i < 2; i++) { + await insertMessage( + createTestMessage({ + id: `msg-nomore-${i}`, + channel_id: channelId, + created_at: now - i * 1000, + }), + ); + } + + const result = await listMessages({ + channelId, + limit: 10, + }); + + expect(result.data).toHaveLength(2); + expect(result.nextCursor).toBeNull(); + }); + + it("second page using nextCursor does not duplicate first page", async () => { + const now = Date.now(); + const channelId = `channel-${Math.random().toString(36).slice(2)}`; + + // Insert 6 messages + const messageIds: string[] = []; + for (let i = 0; i < 6; i++) { + const id = `msg-dup-${i}`; + messageIds.push(id); + await insertMessage( + createTestMessage({ + id, + channel_id: channelId, + created_at: now - i * 1000, + }), + ); + } + + // Get first page + const page1 = await listMessages({ + channelId, + limit: 3, + }); + + expect(page1.data).toHaveLength(3); + expect(page1.nextCursor).not.toBeNull(); + + const page1Ids = page1.data.map((m) => m.id); + + // Get second page using cursor + const page2 = await listMessages({ + channelId, + limit: 3, + cursor: page1.nextCursor!, + }); + + expect(page2.data).toHaveLength(3); + + const page2Ids = page2.data.map((m) => m.id); + + // Verify no overlap + const overlap = page1Ids.filter((id) => page2Ids.includes(id)); + expect(overlap).toHaveLength(0); + + // Verify all messages are accounted for + const allIds = [...page1Ids, ...page2Ids]; + expect(allIds.sort()).toEqual(messageIds.sort()); + }); + + it("filters by channelId correctly", async () => { + const now = Date.now(); + const channel1 = `channel-${Math.random().toString(36).slice(2)}`; + const channel2 = `channel-${Math.random().toString(36).slice(2)}`; + + // Insert messages in two channels + await insertMessage( + createTestMessage({ + id: "msg-ch1-1", + channel_id: channel1, + created_at: now, + }), + ); + await insertMessage( + createTestMessage({ + id: "msg-ch2-1", + channel_id: channel2, + created_at: now, + }), + ); + + const result = await listMessages({ + channelId: channel1, + limit: 10, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].channel_id).toBe(channel1); + }); + + it("filters by status correctly", async () => { + const now = Date.now(); + const channelId = `channel-${Math.random().toString(36).slice(2)}`; + + // Insert messages with different statuses + const msg1 = createTestMessage({ + id: "msg-status-1", + channel_id: channelId, + created_at: now, + ai_status: "clean", + }); + const msg2 = createTestMessage({ + id: "msg-status-2", + channel_id: channelId, + created_at: now - 1000, + ai_status: "warn", + }); + const msg3 = createTestMessage({ + id: "msg-status-3", + channel_id: channelId, + created_at: now - 2000, + ai_status: "flagged", + }); + + await insertMessage(msg1); + await insertMessage(msg2); + await insertMessage(msg3); + + // Query for warn and flagged only + const result = await listMessages({ + channelId, + status: ["warn", "flagged"], + limit: 10, + }); + + expect(result.data).toHaveLength(2); + expect(result.data.map((m) => m.id).sort()).toEqual( + ["msg-status-2", "msg-status-3"].sort(), + ); + }); + + it("filters by channelId and status together", async () => { + const now = Date.now(); + const channel1 = `channel-${Math.random().toString(36).slice(2)}`; + const channel2 = `channel-${Math.random().toString(36).slice(2)}`; + + // Insert messages in two channels with different statuses + await insertMessage( + createTestMessage({ + id: "msg-combo-1", + channel_id: channel1, + created_at: now, + ai_status: "warn", + }), + ); + await insertMessage( + createTestMessage({ + id: "msg-combo-2", + channel_id: channel1, + created_at: now - 1000, + ai_status: "clean", + }), + ); + await insertMessage( + createTestMessage({ + id: "msg-combo-3", + channel_id: channel2, + created_at: now - 2000, + ai_status: "warn", + }), + ); + + // Query for warn status in channel1 only + const result = await listMessages({ + channelId: channel1, + status: ["warn"], + limit: 10, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].id).toBe("msg-combo-1"); + }); +}); + +describe("listReviewMessages integration tests", () => { + beforeAll(async () => { + // Initialize database once for all tests + await initializeDatabase(); + }); + + afterAll(async () => { + try { + await closeDatabase(); + } catch (error) { + logger.error( + { error: error instanceof Error ? error.message : String(error) }, + "Error closing database in afterAll", + ); + } + }); + + beforeEach(async () => { + // Clear messages table before each test + try { + const db = getDatabase(); + await db.delete(messagesTable); + } catch (error) { + logger.debug("Could not clear messages table", { error }); + } + }); + + const createTestMessage = ( + overrides: Partial = {}, + ): MessageRecord => ({ + id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`, + guild_id: "guild-123", + channel_id: "channel-456", + thread_id: null, + user_id: "user-789", + username: "testuser", + avatar_url: null, + content: "Test message", + edited_content: null, + created_at: Date.now(), + edited_at: null, + deleted_at: null, + type: "text", + metadata: null, + ai_status: "pending", + ...overrides, + }); + + it("defaults to warn, flagged, and error statuses", async () => { + const now = Date.now(); + const channelId = `channel-${Math.random().toString(36).slice(2)}`; + + // Insert messages with all statuses + await insertMessage( + createTestMessage({ + id: "msg-review-1", + channel_id: channelId, + created_at: now, + ai_status: "clean", + }), + ); + await insertMessage( + createTestMessage({ + id: "msg-review-2", + channel_id: channelId, + created_at: now - 1000, + ai_status: "warn", + }), + ); + await insertMessage( + createTestMessage({ + id: "msg-review-3", + channel_id: channelId, + created_at: now - 2000, + ai_status: "flagged", + }), + ); + await insertMessage( + createTestMessage({ + id: "msg-review-4", + channel_id: channelId, + created_at: now - 3000, + ai_status: "error", + }), + ); + await insertMessage( + createTestMessage({ + id: "msg-review-5", + channel_id: channelId, + created_at: now - 4000, + ai_status: "pending", + }), + ); + + const result = await listReviewMessages({ + channelId, + limit: 10, + }); + + expect(result.data).toHaveLength(3); + const ids = result.data.map((m) => m.id).sort(); + expect(ids).toEqual(["msg-review-2", "msg-review-3", "msg-review-4"].sort()); + }); + + it("excludes clean status messages", async () => { + const now = Date.now(); + const channelId = `channel-${Math.random().toString(36).slice(2)}`; + + await insertMessage( + createTestMessage({ + id: "msg-clean-1", + channel_id: channelId, + created_at: now, + ai_status: "clean", + }), + ); + await insertMessage( + createTestMessage({ + id: "msg-clean-2", + channel_id: channelId, + created_at: now - 1000, + ai_status: "warn", + }), + ); + + const result = await listReviewMessages({ + channelId, + limit: 10, + }); + + expect(result.data).toHaveLength(1); + expect(result.data[0].id).toBe("msg-clean-2"); + }); + + it("respects pagination with review messages", async () => { + const now = Date.now(); + const channelId = `channel-${Math.random().toString(36).slice(2)}`; + + // Insert 5 review-worthy messages + for (let i = 0; i < 5; i++) { + await insertMessage( + createTestMessage({ + id: `msg-review-page-${i}`, + channel_id: channelId, + created_at: now - i * 1000, + ai_status: i % 2 === 0 ? "warn" : "flagged", + }), + ); + } + + const page1 = await listReviewMessages({ + channelId, + limit: 2, + }); + + expect(page1.data).toHaveLength(2); + expect(page1.nextCursor).not.toBeNull(); + + const page2 = await listReviewMessages({ + channelId, + limit: 2, + cursor: page1.nextCursor!, + }); + + expect(page2.data).toHaveLength(2); + + // Verify no overlap + const page1Ids = page1.data.map((m) => m.id); + const page2Ids = page2.data.map((m) => m.id); + const overlap = page1Ids.filter((id) => page2Ids.includes(id)); + expect(overlap).toHaveLength(0); + }); +});