Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
408 changes: 408 additions & 0 deletions packages/bot/src/bot/start/initializer.spec.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/bot/src/bot/start/initializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ export class BotInitializer {
async shutdown(): Promise<void> {
if (this.client) {
try {
this.client.removeAllListeners()
await this.client.destroy()
this.client = null
this.isInitialized = false
Expand Down
239 changes: 239 additions & 0 deletions packages/bot/src/handlers/eventHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ const infoLogMock = jest.fn()
const debugLogMock = jest.fn()
const captureExceptionMock = jest.fn()
const namedSessionListMock = jest.fn()
const cleanupGuildStateMock = jest.fn()
const aiDevToolkitStartMock = jest.fn()
const handleReactionRolesMock = jest.fn()

jest.mock('../utils/general/interactionReply', () => ({
interactionReply: (...args: unknown[]) => interactionReplyMock(...args),
Expand Down Expand Up @@ -71,13 +74,23 @@ jest.mock('../utils/music/namedSessions', () => ({
},
}))

jest.mock('./player/trackNowPlaying', () => ({
cleanupGuildState: (...args: unknown[]) => cleanupGuildStateMock(...args),
}))

jest.mock('@lucky/shared/utils', () => ({
errorLog: (...args: unknown[]) => errorLogMock(...args),
infoLog: (...args: unknown[]) => infoLogMock(...args),
debugLog: (...args: unknown[]) => debugLogMock(...args),
captureException: (...args: unknown[]) => captureExceptionMock(...args),
}))

jest.mock('../services/AiDevToolkitService', () => ({
aiDevToolkitService: {
start: (...args: unknown[]) => aiDevToolkitStartMock(...args),
},
}))

function createMockClient() {
const onMock = jest.fn()
const onceMock = jest.fn()
Expand Down Expand Up @@ -357,4 +370,230 @@ describe('eventHandler', () => {
expect(handleMusicButtonInteractionMock).not.toHaveBeenCalled()
})
})

describe('guild and channel cleanup', () => {
function getGuildDeleteHandler(
onMock: jest.Mock,
): ((guild: unknown) => Promise<void>) | undefined {
const call = onMock.mock.calls.find((args) => args[0] === Events.GuildDelete)
return call?.[1] as ((guild: unknown) => Promise<void>) | undefined
}

function getChannelDeleteHandler(
onMock: jest.Mock,
): ((channel: unknown) => void) | undefined {
const call = onMock.mock.calls.find(
(args) => args[0] === Events.ChannelDelete,
)
return call?.[1] as ((channel: unknown) => void) | undefined
}

describe('handleGuildDelete', () => {
it('calls cleanupGuildState when guild is deleted', async () => {
const { client, onMock } = createMockClient()
handleEvents(client as unknown as never)

const handler = getGuildDeleteHandler(onMock)
expect(handler).toBeDefined()

const mockGuild = { id: 'guild-delete-123' }
await handler?.(mockGuild)

expect(cleanupGuildStateMock).toHaveBeenCalledWith('guild-delete-123')
})

it('logs error when guild delete cleanup fails', async () => {
const { client, onMock } = createMockClient()
handleEvents(client as unknown as never)

const handler = getGuildDeleteHandler(onMock)
const mockGuild = { id: 'guild-error-456' }

await handler?.(mockGuild)

expect(cleanupGuildStateMock).toHaveBeenCalled()
})
Comment thread
LucasSantana-Dev marked this conversation as resolved.

it('handles errors during guild cleanup gracefully', async () => {
const { client, onMock } = createMockClient()
cleanupGuildStateMock.mockImplementation(() => {
throw new Error('Cleanup failed')
})

handleEvents(client as unknown as never)

const handler = getGuildDeleteHandler(onMock)
const mockGuild = { id: 'guild-789' }

await handler?.(mockGuild)

expect(errorLogMock).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining(
'Error clearing history on guild delete',
),
}),
)
})
})

describe('handleChannelDelete', () => {
it('calls cleanupGuildState when text channel is deleted', () => {
const { client, onMock } = createMockClient()
handleEvents(client as unknown as never)

const handler = getChannelDeleteHandler(onMock)
expect(handler).toBeDefined()

const mockChannel = {
guildId: 'guild-ch-123',
isDMBased: () => false,
}

handler?.(mockChannel)

expect(cleanupGuildStateMock).toHaveBeenCalledWith('guild-ch-123')
})

it('skips cleanup when channel is DM-based', () => {
const { client, onMock } = createMockClient()
handleEvents(client as unknown as never)

const handler = getChannelDeleteHandler(onMock)
const mockChannel = {
guildId: 'guild-unused',
isDMBased: () => true,
}

handler?.(mockChannel)

expect(cleanupGuildStateMock).not.toHaveBeenCalled()
})

it('logs error when channel cleanup fails', () => {
const { client, onMock } = createMockClient()
cleanupGuildStateMock.mockImplementation(() => {
throw new Error('Channel cleanup failed')
})

handleEvents(client as unknown as never)

const handler = getChannelDeleteHandler(onMock)
const mockChannel = {
guildId: 'guild-error-ch',
isDMBased: () => false,
}

handler?.(mockChannel)

expect(errorLogMock).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining(
'Error clearing state on channel delete',
),
}),
)
})
})
})
Comment thread
LucasSantana-Dev marked this conversation as resolved.

describe('client ready', () => {
function getClientReadyHandler(
onceMock: jest.Mock,
): ((client: unknown) => void) | undefined {
const call = onceMock.mock.calls.find((args) => args[0] === 'clientReady')
return call?.[1] as ((client: unknown) => void) | undefined
}

it('logs when client is ready', () => {
const { client, onceMock } = createMockClient()
const mockClient = {
...client,
user: { tag: 'TestBot#0001' },
}

handleEvents(mockClient as unknown as never)

const handler = getClientReadyHandler(onceMock)
expect(handler).toBeDefined()

handler?.({})

expect(infoLogMock).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Logged in as'),
}),
)
})

it('logs command count when ready', () => {
const { client, onceMock } = createMockClient()
client.commands.set('cmd1', { execute: jest.fn() })
client.commands.set('cmd2', { execute: jest.fn() })
const mockClient = {
...client,
user: { tag: 'TestBot#0001' },
}

handleEvents(mockClient as unknown as never)

const handler = getClientReadyHandler(onceMock)
handler?.({})

expect(debugLogMock).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('Bot is ready'),
}),
)
})

it('starts ai dev toolkit service when enabled', async () => {
process.env.AI_DEV_TOOLKIT_BOARD_ENABLED = 'true'
aiDevToolkitStartMock.mockResolvedValue(undefined)

const { client, onceMock } = createMockClient()
const mockClient = {
...client,
user: { tag: 'TestBot#0001' },
}

handleEvents(mockClient as unknown as never)

const handler = getClientReadyHandler(onceMock)
handler?.(mockClient)

await new Promise<void>((resolve) => setImmediate(resolve))

expect(aiDevToolkitStartMock).toHaveBeenCalled()

delete process.env.AI_DEV_TOOLKIT_BOARD_ENABLED
})

it('logs error if ai dev toolkit fails to start', async () => {
process.env.AI_DEV_TOOLKIT_BOARD_ENABLED = 'true'
aiDevToolkitStartMock.mockRejectedValue(new Error('Toolkit start failed'))

const { client, onceMock } = createMockClient()
const mockClient = {
...client,
user: { tag: 'TestBot#0001' },
}

handleEvents(mockClient as unknown as never)

const handler = getClientReadyHandler(onceMock)
handler?.(mockClient)

await new Promise<void>((resolve) => setImmediate(resolve))

expect(errorLogMock).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.stringContaining('AiDevToolkitService'),
}),
)

delete process.env.AI_DEV_TOOLKIT_BOARD_ENABLED
})
})
})

17 changes: 17 additions & 0 deletions packages/bot/src/handlers/eventHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { handleMusicButtonInteraction } from './musicButtonHandler'
import { reactionRolesService } from '@lucky/shared/services'
import { aiDevToolkitService } from '../services/AiDevToolkitService'
import { namedSessionService } from '../utils/music/namedSessions'
import { cleanupGuildState } from './player/trackNowPlaying'

function handleClientReady(client: Client): void {
client.once('clientReady', () => {
Expand Down Expand Up @@ -203,6 +204,7 @@ function handleGuildDelete(client: Client): void {
}
duplicateDetection.clearHistory(guild.id)
duplicateDetection.clearAllGuildCaches(guild.id)
cleanupGuildState(guild.id)
} catch (err) {
errorLog({
message: 'Error clearing history on guild delete:',
Expand All @@ -212,6 +214,20 @@ function handleGuildDelete(client: Client): void {
})
}

function handleChannelDelete(client: Client): void {
client.on(Events.ChannelDelete, (channel) => {
try {
if (channel.isDMBased()) return
cleanupGuildState(channel.guildId)
} catch (err) {
errorLog({
message: 'Error clearing state on channel delete:',
error: err,
})
}
})
}

export default function handleEvents(client: Client) {
handleClientReady(client)
client.on(Events.InteractionCreate, (interaction: Interaction) => {
Expand All @@ -228,4 +244,5 @@ export default function handleEvents(client: Client) {
handleWarn(client)
handleDebug(client)
handleGuildDelete(client)
handleChannelDelete(client)
}
Loading
Loading