Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -515,23 +515,37 @@ describe('bulkMoveMessages command', () => {
expect(errorLogMock).toHaveBeenCalled()
})

test('samples messages to estimate count for "all" scope', async () => {
test('samples messages to estimate count for "all" scope with proper pagination', async () => {
matchesScopeMock.mockReturnValue(true)

const sourceChannel = createMockChannel()
sourceChannel.messages.fetch = jest.fn().mockResolvedValue(
new Map(
Array.from({ length: 500 }, (_, i) => [
`msg-${i}`,
{
id: `msg-${i}`,
author: { id: 'user-1' },
content: 'test',
createdAt: new Date(),
},
]),
),
)

// Mock pagination: return 5 pages of 100 messages each, then empty
let callCount = 0
sourceChannel.messages.fetch = jest.fn().mockImplementation(() => {
callCount++
if (callCount <= 5) {
// Return 100 messages per page
return Promise.resolve(
new Map(
Array.from({ length: 100 }, (_, i) => {
const msgIndex = (callCount - 1) * 100 + i
return [
`msg-${msgIndex}`,
{
id: `msg-${msgIndex}`,
author: { id: 'user-1' },
content: 'test',
createdAt: new Date(),
},
]
}),
),
)
}
// Return empty map (end of messages)
return Promise.resolve(new Map())
})

const interaction = createInteraction({
sourceChannel,
Expand All @@ -543,9 +557,26 @@ describe('bulkMoveMessages command', () => {

await bulkMoveMessagesCommand.execute({ interaction } as any)

expect(sourceChannel.messages.fetch).toHaveBeenCalledWith(
expect.objectContaining({ limit: 500 }),
)
// Should fetch with limit ≤ 100 (the Discord API limit)
const fetchCalls = (sourceChannel.messages.fetch as jest.Mock).mock
.calls
expect(fetchCalls.length).toBeGreaterThan(0)

// Each call should respect the Discord limit of 100
for (const [callArgs] of fetchCalls) {
expect(callArgs.limit).toBeLessThanOrEqual(100)
}

// Should have paginated through multiple calls
expect(fetchCalls.length).toBeGreaterThan(1)

// First call should not have a cursor
expect(fetchCalls[0][0].before).toBeUndefined()

// Subsequent calls should have a cursor for pagination
if (fetchCalls.length > 1) {
expect(fetchCalls[1][0].before).toBeDefined()
}
})

test('uses exact count for "count" scope', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
SlashCommandBuilder,
PermissionFlagsBits,
type ChatInputCommandInteraction,

Check warning on line 4 in packages/bot/src/functions/moderation/commands/bulkMoveMessages.ts

View workflow job for this annotation

GitHub Actions / quality / Lint (lint)

'ChatInputCommandInteraction' is defined but never used. Allowed unused vars must match /^_/u
type GuildTextBasedChannel,
type Collection,
type Message,
Expand Down Expand Up @@ -248,19 +248,46 @@
return
}

// Estimate total by sampling (cap at 500 fetches for performance)
// Estimate total by sampling (cap at 500 messages for performance)
await interaction.deferReply({ ephemeral: true })

let totalEstimate = 0

Check warning on line 254 in packages/bot/src/functions/moderation/commands/bulkMoveMessages.ts

View workflow job for this annotation

GitHub Actions / quality / Lint (lint)

The value assigned to 'totalEstimate' is not used in subsequent statements
const SAMPLE_SIZE = 500
const PAGE_SIZE = 100
try {
const sampleMessages = (await sourceChannel.messages.fetch({
limit: SAMPLE_SIZE,
})) as Collection<string, Message<true>>

let sampleMatched = 0
let messageIndex = 0
for (const msg of sampleMessages.values()) {
let cursor: string | undefined
const allSampleMessages: Message<true>[] = []

// Paginate through messages in pages of ≤100 (Discord API limit)
// until we collect SAMPLE_SIZE messages or reach the end
while (allSampleMessages.length < SAMPLE_SIZE) {
const pageMessages = (await sourceChannel.messages.fetch({
limit: PAGE_SIZE,
before: cursor,
})) as Collection<string, Message<true>>

if (pageMessages.size === 0) {
break
}

// Convert to array and add to collection
const messageArray = Array.from(pageMessages.values())
allSampleMessages.push(...messageArray)

// Update cursor for next page
cursor = messageArray[messageArray.length - 1]?.id

Check warning on line 280 in packages/bot/src/functions/moderation/commands/bulkMoveMessages.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `.at(…)` over `[….length - index]`.

See more on https://sonarcloud.io/project/issues?id=LucasSantana-Dev_Lucky&issues=AZ9NN8DGqwNy1pUX1Y6F&open=AZ9NN8DGqwNy1pUX1Y6F&pullRequest=1776

// Stop if we've collected enough samples
if (allSampleMessages.length >= SAMPLE_SIZE) {
allSampleMessages.splice(SAMPLE_SIZE)
break
}
}

// Count matches in the sample
for (const msg of allSampleMessages) {
const matches = matchesScope(
{
id: msg.id,
Expand Down
Loading