import { beforeEach, describe, expect, mock, it } from 'bun:test'
const mockSendGroupMessage = mock(() =>
Promise.resolve({
id: 'msg-group-1',
channelId: 'ws-1',
chatId: 'grp-1',
chatType: 'group',
personType: 'manager',
personId: 'mgr-1',
createdAt: 1000,
plainText: 'Hello group',
}),
)
const mockSendUserChatMessage = mock(() =>
Promise.resolve({
id: 'msg-user-1',
channelId: 'ws-1',
chatId: 'chat-1',
chatType: 'userChat',
personType: 'manager',
personId: 'mgr-1',
createdAt: 1001,
plainText: 'Hello user chat',
}),
)
const mockSendDirectChatMessage = mock(() =>
Promise.resolve({
id: 'msg-direct-1',
channelId: 'ws-1',
chatId: 'dm-1',
chatType: 'directChat',
personType: 'manager',
personId: 'mgr-1',
createdAt: 1002,
plainText: 'Hello direct chat',
}),
)
const mockGetGroupMessages = mock(() =>
Promise.resolve([
{
id: 'msg-group-list-1',
channelId: 'ws-1',
chatId: 'grp-1',
chatType: 'group',
personType: 'manager',
personId: 'mgr-1',
createdAt: 2000,
plainText: 'Group message',
blocks: [{ type: 'text', content: [{ type: 'plain', attrs: { text: 'Group message' } }] }],
},
]),
)
const mockGetUserChatMessages = mock(() =>
Promise.resolve([
{
id: 'msg-user-list-1',
channelId: 'ws-1',
chatId: 'chat-1',
chatType: 'userChat',
personType: 'manager',
personId: 'mgr-1',
createdAt: 2001,
plainText: 'User chat message',
blocks: [{ type: 'text', content: [{ type: 'plain', attrs: { text: 'User chat message' } }] }],
},
]),
)
const mockGetDirectChatMessages = mock(() =>
Promise.resolve([
{
id: 'msg-direct-list-1',
channelId: 'ws-1',
chatId: 'dm-1',
chatType: 'directChat',
personType: 'manager',
personId: 'mgr-1',
createdAt: 2002,
plainText: 'Direct chat message',
blocks: [{ type: 'text', content: [{ type: 'plain', attrs: { text: 'Direct chat message' } }] }],
},
]),
)
const mockSearchTeamChatMessages = mock(() =>
Promise.resolve({
hits: [
{
index: 'messages-2026-03',
score: 'NaN',
source: {
id: 'msg-search-1',
channelId: 'ws-1',
chatType: 'group',
chatId: 'grp-1',
personType: 'manager',
personId: 'mgr-1',
createdAt: 3000,
plainText: 'search result message',
},
highlight: {
plainText: { name: 'plainText', fragments: ['search result message'] },
},
searchAfter: [3000, 'msg-search-1'],
},
],
bots: [],
sessions: [],
groups: [],
}),
)
const mockSearchUserChatMessages = mock(() =>
Promise.resolve({
hits: [
{
index: 'messages-2026-03',
score: 'NaN',
source: {
id: 'msg-search-2',
channelId: 'ws-1',
chatType: 'userChat',
chatId: 'chat-1',
personType: 'manager',
personId: 'mgr-1',
createdAt: 3001,
plainText: 'user chat search result',
},
highlight: {
plainText: { name: 'plainText', fragments: ['user chat search result'] },
},
searchAfter: [3001, 'msg-search-2'],
},
],
bots: [],
sessions: [],
userChats: [],
}),
)
mock.module('./shared', () => ({
getClient: async () => ({
sendGroupMessage: mockSendGroupMessage,
sendUserChatMessage: mockSendUserChatMessage,
sendDirectChatMessage: mockSendDirectChatMessage,
getGroupMessages: mockGetGroupMessages,
getUserChatMessages: mockGetUserChatMessages,
getDirectChatMessages: mockGetDirectChatMessages,
searchTeamChatMessages: mockSearchTeamChatMessages,
searchUserChatMessages: mockSearchUserChatMessages,
}),
getCurrentWorkspaceId: async () => 'ws-1',
}))
import { getAction, listAction, searchAction, sendAction } from './message'
describe('message commands', () => {
beforeEach(() => {
mockSendGroupMessage.mockReset()
mockSendGroupMessage.mockImplementation(() =>
Promise.resolve({
id: 'msg-group-1',
channelId: 'ws-1',
chatId: 'grp-1',
chatType: 'group',
personType: 'manager',
personId: 'mgr-1',
createdAt: 1000,
plainText: 'Hello group',
}),
)
mockSendUserChatMessage.mockReset()
mockSendUserChatMessage.mockImplementation(() =>
Promise.resolve({
id: 'msg-user-1',
channelId: 'ws-1',
chatId: 'chat-1',
chatType: 'userChat',
personType: 'manager',
personId: 'mgr-1',
createdAt: 1001,
plainText: 'Hello user chat',
}),
)
mockSendDirectChatMessage.mockReset()
mockSendDirectChatMessage.mockImplementation(() =>
Promise.resolve({
id: 'msg-direct-1',
channelId: 'ws-1',
chatId: 'dm-1',
chatType: 'directChat',
personType: 'manager',
personId: 'mgr-1',
createdAt: 1002,
plainText: 'Hello direct chat',
}),
)
mockGetGroupMessages.mockReset()
mockGetGroupMessages.mockImplementation(() =>
Promise.resolve([
{
id: 'msg-group-list-1',
channelId: 'ws-1',
chatId: 'grp-1',
chatType: 'group',
personType: 'manager',
personId: 'mgr-1',
createdAt: 2000,
plainText: 'Group message',
blocks: [{ type: 'text', content: [{ type: 'plain', attrs: { text: 'Group message' } }] }],
},
]),
)
mockGetUserChatMessages.mockReset()
mockGetUserChatMessages.mockImplementation(() =>
Promise.resolve([
{
id: 'msg-user-list-1',
channelId: 'ws-1',
chatId: 'chat-1',
chatType: 'userChat',
personType: 'manager',
personId: 'mgr-1',
createdAt: 2001,
plainText: 'User chat message',
blocks: [{ type: 'text', content: [{ type: 'plain', attrs: { text: 'User chat message' } }] }],
},
]),
)
mockGetDirectChatMessages.mockReset()
mockGetDirectChatMessages.mockImplementation(() =>
Promise.resolve([
{
id: 'msg-direct-list-1',
channelId: 'ws-1',
chatId: 'dm-1',
chatType: 'directChat',
personType: 'manager',
personId: 'mgr-1',
createdAt: 2002,
plainText: 'Direct chat message',
blocks: [{ type: 'text', content: [{ type: 'plain', attrs: { text: 'Direct chat message' } }] }],
},
]),
)
mockSearchTeamChatMessages.mockReset()
mockSearchTeamChatMessages.mockImplementation(() =>
Promise.resolve({
hits: [
{
index: 'messages-2026-03',
score: 'NaN',
source: {
id: 'msg-search-1',
channelId: 'ws-1',
chatType: 'group',
chatId: 'grp-1',
personType: 'manager',
personId: 'mgr-1',
createdAt: 3000,
plainText: 'search result message',
},
highlight: {
plainText: { name: 'plainText', fragments: ['search result message'] },
},
searchAfter: [3000, 'msg-search-1'],
},
],
bots: [],
sessions: [],
groups: [],
}),
)
mockSearchUserChatMessages.mockReset()
mockSearchUserChatMessages.mockImplementation(() =>
Promise.resolve({
hits: [
{
index: 'messages-2026-03',
score: 'NaN',
source: {
id: 'msg-search-2',
channelId: 'ws-1',
chatType: 'userChat',
chatId: 'chat-1',
personType: 'manager',
personId: 'mgr-1',
createdAt: 3001,
plainText: 'user chat search result',
},
highlight: {
plainText: { name: 'plainText', fragments: ['user chat search result'] },
},
searchAfter: [3001, 'msg-search-2'],
},
],
bots: [],
sessions: [],
userChats: [],
}),
)
})
it('sendAction sends a group message with wrapped blocks', async () => {
const result = await sendAction('group', 'grp-1', 'Hello group')
expect(mockSendGroupMessage).toHaveBeenCalledWith('ws-1', 'grp-1', [{ type: 'text', value: 'Hello group' }])
expect(result).toMatchObject({
id: 'msg-group-1',
channel_id: 'ws-1',
chat_id: 'grp-1',
chat_type: 'group',
plain_text: 'Hello group',
})
})
it('sendAction sends a user chat message with wrapped blocks', async () => {
const result = await sendAction('user-chat', 'chat-1', 'Hello user chat')
expect(mockSendUserChatMessage).toHaveBeenCalledWith('ws-1', 'chat-1', [{ type: 'text', value: 'Hello user chat' }])
expect(result).toMatchObject({
id: 'msg-user-1',
channel_id: 'ws-1',
chat_id: 'chat-1',
chat_type: 'userChat',
plain_text: 'Hello user chat',
})
})
it('sendAction sends a direct chat message with wrapped blocks', async () => {
const result = await sendAction('direct-chat', 'dm-1', 'Hello direct chat')
expect(mockSendDirectChatMessage).toHaveBeenCalledWith('ws-1', 'dm-1', [
{ type: 'text', value: 'Hello direct chat' },
])
expect(result).toMatchObject({
id: 'msg-direct-1',
channel_id: 'ws-1',
chat_id: 'dm-1',
chat_type: 'directChat',
plain_text: 'Hello direct chat',
})
})
it('listAction lists group messages with limit and sort options', async () => {
const result = await listAction('group', 'grp-1', { limit: '10', sort: 'asc' })
expect(mockGetGroupMessages).toHaveBeenCalledWith('ws-1', 'grp-1', { limit: 10, sortOrder: 'asc' })
expect(result.messages).toEqual([
{
id: 'msg-group-list-1',
channel_id: 'ws-1',
chat_id: 'grp-1',
chat_type: 'group',
person_type: 'manager',
person_id: 'mgr-1',
created_at: 2000,
plain_text: 'Group message',
},
])
})
it('listAction lists user chat messages', async () => {
const result = await listAction('user-chat', 'chat-1')
expect(mockGetUserChatMessages).toHaveBeenCalledWith('ws-1', 'chat-1', { limit: 25, sortOrder: 'desc' })
expect(result.messages?.[0]).toMatchObject({
id: 'msg-user-list-1',
chat_id: 'chat-1',
plain_text: 'User chat message',
})
})
it('listAction lists direct chat messages', async () => {
const result = await listAction('direct-chat', 'dm-1')
expect(mockGetDirectChatMessages).toHaveBeenCalledWith('ws-1', 'dm-1', { limit: 25, sortOrder: 'desc' })
expect(result.messages?.[0]).toMatchObject({
id: 'msg-direct-list-1',
chat_id: 'dm-1',
plain_text: 'Direct chat message',
})
})
it('getAction returns specific message by ID', async () => {
const result = await getAction('group', 'grp-1', 'msg-group-list-1')
expect(result.error).toBeUndefined()
expect(result.id).toBe('msg-group-list-1')
})
it('getAction returns error when message not found', async () => {
const result = await getAction('group', 'grp-1', 'nonexistent')
expect(result.error).toBeDefined()
expect(result.error).toContain('not found')
})
it('searchAction searches team chat messages by default', async () => {
const result = await searchAction('search')
expect(mockSearchTeamChatMessages).toHaveBeenCalledWith('ws-1', 'search', { limit: undefined })
expect(result.results).toEqual([
{
id: 'msg-search-1',
channel_id: 'ws-1',
chat_type: 'group',
chat_id: 'grp-1',
person_type: 'manager',
person_id: 'mgr-1',
created_at: 3000,
plain_text: 'search result message',
highlight: ['search result message'],
},
])
})
it('searchAction searches user chat messages with scope option', async () => {
const result = await searchAction('search', { scope: 'user-chat' })
expect(mockSearchUserChatMessages).toHaveBeenCalledWith('ws-1', 'search', { limit: undefined })
expect(result.results).toEqual([
{
id: 'msg-search-2',
channel_id: 'ws-1',
chat_type: 'userChat',
chat_id: 'chat-1',
person_type: 'manager',
person_id: 'mgr-1',
created_at: 3001,
plain_text: 'user chat search result',
highlight: ['user chat search result'],
},
])
})
it('searchAction passes limit parameter', async () => {
await searchAction('test', { limit: '5' })
expect(mockSearchTeamChatMessages).toHaveBeenCalledWith('ws-1', 'test', { limit: 5 })
})
it('searchAction returns error on failure', async () => {
mockSearchTeamChatMessages.mockImplementation(() => Promise.reject(new Error('Network error')))
const result = await searchAction('test')
expect(result.error).toBe('Network error')
expect(result.results).toBeUndefined()
})
it('searchAction returns error for invalid scope', async () => {
const result = await searchAction('test', { scope: 'invalid' })
expect(result.error).toContain('Invalid --scope value')
expect(result.error).toContain('invalid')
expect(result.results).toBeUndefined()
})
it('searchAction handles zero hits', async () => {
mockSearchTeamChatMessages.mockImplementation(() =>
Promise.resolve({ hits: [], bots: [], sessions: [], groups: [] }),
)
const result = await searchAction('no-match')
expect(result.results).toEqual([])
expect(result.error).toBeUndefined()
})
it('searchAction handles missing highlight plainText key', async () => {
mockSearchTeamChatMessages.mockImplementation(() =>
Promise.resolve({
hits: [
{
index: 'messages-2026-03',
score: 'NaN',
source: {
id: 'msg-no-highlight',
channelId: 'ws-1',
chatType: 'group',
chatId: 'grp-1',
personType: 'manager',
personId: 'mgr-1',
createdAt: 4000,
plainText: 'some message',
},
highlight: {},
searchAfter: [4000, 'msg-no-highlight'],
},
],
bots: [],
sessions: [],
groups: [],
}),
)
const result = await searchAction('test')
expect(result.results?.[0]?.highlight).toBeUndefined()
expect(result.results?.[0]?.plain_text).toBe('some message')
})
it('searchAction uses extractText for block-only messages', async () => {
mockSearchTeamChatMessages.mockImplementation(() =>
Promise.resolve({
hits: [
{
index: 'messages-2026-03',
score: 'NaN',
source: {
id: 'msg-blocks-only',
channelId: 'ws-1',
chatType: 'group',
chatId: 'grp-1',
personType: 'manager',
personId: 'mgr-1',
createdAt: 5000,
blocks: [{ type: 'text', value: 'block content' }],
},
highlight: {
plainText: { name: 'plainText', fragments: ['block content'] },
},
searchAfter: [5000, 'msg-blocks-only'],
},
],
bots: [],
sessions: [],
groups: [],
}),
)
const result = await searchAction('block')
expect(result.results?.[0]?.plain_text).toBe('block content')
})
})