test: cover message query pagination

This commit is contained in:
MythEclipse
2026-05-14 19:00:03 +07:00
parent 0c54ac4ba8
commit 7a8883f623

View File

@@ -1,4 +1,4 @@
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { import {
decodeCursor, decodeCursor,
encodeCursor, encodeCursor,
@@ -24,10 +24,41 @@ describe("message cursor helpers", () => {
}); });
}); });
describe("listMessages integration tests", () => { describe("message query integration tests", () => {
beforeAll(async () => { beforeAll(async () => {
// Initialize database once for all tests
await initializeDatabase(); 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 () => { afterAll(async () => {
@@ -41,451 +72,417 @@ describe("listMessages integration tests", () => {
} }
}); });
beforeEach(async () => { describe("listMessages", () => {
// Clear messages table before each test const createTestMessage = (
try { overrides: Partial<MessageRecord> = {},
const db = getDatabase(); ): MessageRecord => ({
await db.delete(messagesTable); id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`,
} catch (error) { guild_id: "guild-123",
logger.debug("Could not clear messages table", { error }); channel_id: "channel-456",
} thread_id: null,
}); user_id: "user-789",
username: "testuser",
const createTestMessage = ( avatar_url: null,
overrides: Partial<MessageRecord> = {}, content: "Test message",
): MessageRecord => ({ edited_content: null,
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2)}`, created_at: Date.now(),
guild_id: "guild-123", edited_at: null,
channel_id: "channel-456", deleted_at: null,
thread_id: null, type: "text",
user_id: "user-789", metadata: null,
username: "testuser", ai_status: "pending",
avatar_url: null, ...overrides,
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); it("returns messages in newest-first order", async () => {
await insertMessage(msg2); const now = Date.now();
await insertMessage(msg3); const msg1 = createTestMessage({
id: "msg-1",
const result = await listMessages({ created_at: now - 3000,
channelId: "channel-456", content: "oldest",
limit: 10, });
}); const msg2 = createTestMessage({
id: "msg-2",
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, 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 await insertMessage(msg1);
const result = await listMessages({ await insertMessage(msg2);
channelId: channel1, await insertMessage(msg3);
status: ["warn"],
limit: 10, 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); it("returns nextCursor when more results exist than limit", async () => {
expect(result.data[0].id).toBe("msg-combo-1"); const now = Date.now();
}); const channelId = `channel-${Math.random().toString(36).slice(2)}`;
});
describe("listReviewMessages integration tests", () => { // Insert 5 messages
beforeAll(async () => { for (let i = 0; i < 5; i++) {
// Initialize database once for all tests await insertMessage(
await initializeDatabase(); createTestMessage({
}); id: `msg-limit-${i}`,
channel_id: channelId,
created_at: now - i * 1000,
}),
);
}
afterAll(async () => { const result = await listMessages({
try { channelId,
await closeDatabase(); limit: 3,
} catch (error) { });
logger.error(
{ error: error instanceof Error ? error.message : String(error) }, expect(result.data).toHaveLength(3);
"Error closing database in afterAll", 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 () => { const result = await listMessages({
// Clear messages table before each test channelId: channel1,
try { limit: 10,
const db = getDatabase(); });
await db.delete(messagesTable);
} catch (error) {
logger.debug("Could not clear messages table", { error });
}
});
const createTestMessage = ( expect(result.data).toHaveLength(1);
overrides: Partial<MessageRecord> = {}, expect(result.data[0].channel_id).toBe(channel1);
): 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 () => { it("filters by status correctly", async () => {
const now = Date.now(); const now = Date.now();
const channelId = `channel-${Math.random().toString(36).slice(2)}`; const channelId = `channel-${Math.random().toString(36).slice(2)}`;
// Insert messages with all statuses // Insert messages with different statuses
await insertMessage( const msg1 = createTestMessage({
createTestMessage({ id: "msg-status-1",
id: "msg-review-1",
channel_id: channelId, channel_id: channelId,
created_at: now, created_at: now,
ai_status: "clean", ai_status: "clean",
}), });
); const msg2 = createTestMessage({
await insertMessage( id: "msg-status-2",
createTestMessage({
id: "msg-review-2",
channel_id: channelId, channel_id: channelId,
created_at: now - 1000, created_at: now - 1000,
ai_status: "warn", ai_status: "warn",
}), });
); const msg3 = createTestMessage({
await insertMessage( id: "msg-status-3",
createTestMessage({
id: "msg-review-3",
channel_id: channelId, channel_id: channelId,
created_at: now - 2000, created_at: now - 2000,
ai_status: "flagged", 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({ await insertMessage(msg1);
channelId, await insertMessage(msg2);
limit: 10, 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); it("filters by channelId and status together", async () => {
const ids = result.data.map((m) => m.id).sort(); const now = Date.now();
expect(ids).toEqual(["msg-review-2", "msg-review-3", "msg-review-4"].sort()); const channel1 = `channel-${Math.random().toString(36).slice(2)}`;
}); const channel2 = `channel-${Math.random().toString(36).slice(2)}`;
it("excludes clean status messages", async () => { // Insert messages in two channels with different statuses
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( await insertMessage(
createTestMessage({ createTestMessage({
id: `msg-review-page-${i}`, id: "msg-combo-1",
channel_id: channelId, channel_id: channel1,
created_at: now - i * 1000, created_at: now,
ai_status: i % 2 === 0 ? "warn" : "flagged", 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({ // Query for warn status in channel1 only
channelId, const result = await listMessages({
limit: 2, 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> = {},
): 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); it("defaults to warn, flagged, and error statuses", async () => {
expect(page1.nextCursor).not.toBeNull(); const now = Date.now();
const channelId = `channel-${Math.random().toString(36).slice(2)}`;
const page2 = await listReviewMessages({ // Insert messages with all statuses
channelId, await insertMessage(
limit: 2, createTestMessage({
cursor: page1.nextCursor!, 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 await insertMessage(
const page1Ids = page1.data.map((m) => m.id); createTestMessage({
const page2Ids = page2.data.map((m) => m.id); id: "msg-clean-1",
const overlap = page1Ids.filter((id) => page2Ids.includes(id)); channel_id: channelId,
expect(overlap).toHaveLength(0); 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);
});
}); });
}); });