From 7a8883f62301c56dc5915b90842eddc0f1b4540c Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Thu, 14 May 2026 19:00:03 +0700 Subject: [PATCH] test: cover message query pagination --- tests/moderation/messageStoreQueries.test.ts | 821 +++++++++---------- 1 file changed, 409 insertions(+), 412 deletions(-) diff --git a/tests/moderation/messageStoreQueries.test.ts b/tests/moderation/messageStoreQueries.test.ts index e6b1029..46555d6 100644 --- a/tests/moderation/messageStoreQueries.test.ts +++ b/tests/moderation/messageStoreQueries.test.ts @@ -1,4 +1,4 @@ -import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { decodeCursor, encodeCursor, @@ -24,10 +24,41 @@ describe("message cursor helpers", () => { }); }); -describe("listMessages integration tests", () => { +describe("message query integration tests", () => { beforeAll(async () => { - // Initialize database once for all tests await initializeDatabase(); + // Create tables using Drizzle schema (SQLite doesn't support migrations with PostgreSQL syntax) + const db = getDatabase() as any; + try { + // Create messages table + await 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 + ) + `); + } catch (error) { + logger.debug("Messages table already exists or error creating it", { error }); + } }); afterAll(async () => { @@ -41,451 +72,417 @@ describe("listMessages integration tests", () => { } }); - 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", + describe("listMessages", () => { + 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, }); - 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, + 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, - ai_status: "warn", - }), - ); + content: "middle", + }); + const msg3 = createTestMessage({ + id: "msg-3", + created_at: now - 1000, + content: "newest", + }); - // Query for warn status in channel1 only - const result = await listMessages({ - channelId: channel1, - status: ["warn"], - limit: 10, + 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"); }); - expect(result.data).toHaveLength(1); - expect(result.data[0].id).toBe("msg-combo-1"); - }); -}); + it("returns nextCursor when more results exist than limit", async () => { + const now = Date.now(); + const channelId = `channel-${Math.random().toString(36).slice(2)}`; -describe("listReviewMessages integration tests", () => { - beforeAll(async () => { - // Initialize database once for all tests - await initializeDatabase(); - }); + // 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, + }), + ); + } - afterAll(async () => { - try { - await closeDatabase(); - } catch (error) { - logger.error( - { error: error instanceof Error ? error.message : String(error) }, - "Error closing database in afterAll", + 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, + }), ); - } - }); - 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 result = await listMessages({ + channelId: channel1, + limit: 10, + }); - 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, - }); + expect(result.data).toHaveLength(1); + expect(result.data[0].channel_id).toBe(channel1); + }); - it("defaults to warn, flagged, and error statuses", async () => { - const now = Date.now(); - const channelId = `channel-${Math.random().toString(36).slice(2)}`; + it("filters by status correctly", 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", + // Insert messages with different statuses + const msg1 = createTestMessage({ + id: "msg-status-1", channel_id: channelId, created_at: now, ai_status: "clean", - }), - ); - await insertMessage( - createTestMessage({ - id: "msg-review-2", + }); + const msg2 = createTestMessage({ + id: "msg-status-2", channel_id: channelId, created_at: now - 1000, ai_status: "warn", - }), - ); - await insertMessage( - createTestMessage({ - id: "msg-review-3", + }); + const msg3 = createTestMessage({ + id: "msg-status-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, + 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(), + ); }); - 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("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)}`; - 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++) { + // Insert messages in two channels with different statuses await insertMessage( createTestMessage({ - id: `msg-review-page-${i}`, - channel_id: channelId, - created_at: now - i * 1000, - ai_status: i % 2 === 0 ? "warn" : "flagged", + 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", }), ); - } - const page1 = await listReviewMessages({ - channelId, - limit: 2, + // 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", () => { + 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, }); - expect(page1.data).toHaveLength(2); - expect(page1.nextCursor).not.toBeNull(); + it("defaults to warn, flagged, and error statuses", async () => { + const now = Date.now(); + const channelId = `channel-${Math.random().toString(36).slice(2)}`; - const page2 = await listReviewMessages({ - channelId, - limit: 2, - cursor: page1.nextCursor!, + // 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()); }); - expect(page2.data).toHaveLength(2); + it("excludes clean status messages", async () => { + const now = Date.now(); + const channelId = `channel-${Math.random().toString(36).slice(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); + 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); + }); }); });