@urugus/slack-cli 0.2.12 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/dist/commands/history-display.d.ts +5 -1
- package/dist/commands/history-display.d.ts.map +1 -1
- package/dist/commands/history-display.js +3 -3
- package/dist/commands/history-display.js.map +1 -1
- package/dist/commands/history.d.ts.map +1 -1
- package/dist/commands/history.js +28 -11
- package/dist/commands/history.js.map +1 -1
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +51 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types/commands.d.ts +10 -0
- package/dist/types/commands.d.ts.map +1 -1
- package/dist/utils/constants.d.ts +5 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +5 -0
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/formatters/search-formatters.d.ts +10 -0
- package/dist/utils/formatters/search-formatters.d.ts.map +1 -0
- package/dist/utils/formatters/search-formatters.js +91 -0
- package/dist/utils/formatters/search-formatters.js.map +1 -0
- package/dist/utils/slack-api-client.d.ts +5 -0
- package/dist/utils/slack-api-client.d.ts.map +1 -1
- package/dist/utils/slack-api-client.js +8 -0
- package/dist/utils/slack-api-client.js.map +1 -1
- package/dist/utils/slack-operations/index.d.ts +1 -0
- package/dist/utils/slack-operations/index.d.ts.map +1 -1
- package/dist/utils/slack-operations/index.js +3 -1
- package/dist/utils/slack-operations/index.js.map +1 -1
- package/dist/utils/slack-operations/message-operations.d.ts +1 -0
- package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/message-operations.js +21 -0
- package/dist/utils/slack-operations/message-operations.js.map +1 -1
- package/dist/utils/slack-operations/search-operations.d.ts +29 -0
- package/dist/utils/slack-operations/search-operations.d.ts.map +1 -0
- package/dist/utils/slack-operations/search-operations.js +37 -0
- package/dist/utils/slack-operations/search-operations.js.map +1 -0
- package/dist/utils/validators.d.ts +16 -0
- package/dist/utils/validators.d.ts.map +1 -1
- package/dist/utils/validators.js +50 -0
- package/dist/utils/validators.js.map +1 -1
- package/package.json +5 -2
- package/.claude/settings.local.json +0 -75
- package/.github/dependabot.yml +0 -18
- package/.github/workflows/ci.yml +0 -70
- package/.github/workflows/pr-validation.yml +0 -41
- package/.prettierignore +0 -11
- package/.prettierrc +0 -10
- package/CHANGELOG.md +0 -61
- package/CLAUDE.md +0 -16
- package/eslint.config.js +0 -38
- package/src/commands/channels.ts +0 -50
- package/src/commands/config-subcommands.ts +0 -63
- package/src/commands/config.ts +0 -50
- package/src/commands/history-display.ts +0 -19
- package/src/commands/history-validators.ts +0 -46
- package/src/commands/history.ts +0 -61
- package/src/commands/scheduled.ts +0 -71
- package/src/commands/send.ts +0 -69
- package/src/commands/unread.ts +0 -122
- package/src/index.ts +0 -27
- package/src/types/commands.ts +0 -58
- package/src/types/config.ts +0 -20
- package/src/utils/channel-formatter.ts +0 -45
- package/src/utils/channel-resolver.ts +0 -82
- package/src/utils/client-factory.ts +0 -10
- package/src/utils/command-wrapper.ts +0 -27
- package/src/utils/config/config-file-manager.ts +0 -56
- package/src/utils/config/profile-manager.ts +0 -79
- package/src/utils/config/token-crypto-service.ts +0 -80
- package/src/utils/config-helper.ts +0 -21
- package/src/utils/constants.ts +0 -78
- package/src/utils/date-utils.ts +0 -8
- package/src/utils/error-utils.ts +0 -6
- package/src/utils/errors.ts +0 -33
- package/src/utils/format-utils.ts +0 -9
- package/src/utils/formatters/base-formatter.ts +0 -34
- package/src/utils/formatters/channel-formatters.ts +0 -71
- package/src/utils/formatters/channels-list-formatters.ts +0 -55
- package/src/utils/formatters/history-formatters.ts +0 -123
- package/src/utils/formatters/message-formatters.ts +0 -85
- package/src/utils/mention-utils.ts +0 -47
- package/src/utils/option-parsers.ts +0 -100
- package/src/utils/profile-config.ts +0 -161
- package/src/utils/schedule-utils.ts +0 -41
- package/src/utils/slack-api-client.ts +0 -135
- package/src/utils/slack-operations/base-client.ts +0 -30
- package/src/utils/slack-operations/channel-operations.ts +0 -161
- package/src/utils/slack-operations/index.ts +0 -3
- package/src/utils/slack-operations/message-operations.ts +0 -176
- package/src/utils/slack-patterns.ts +0 -9
- package/src/utils/token-utils.ts +0 -17
- package/src/utils/validators.ts +0 -263
- package/tests/commands/channels.test.ts +0 -250
- package/tests/commands/config.test.ts +0 -158
- package/tests/commands/history.test.ts +0 -403
- package/tests/commands/scheduled.test.ts +0 -131
- package/tests/commands/send.test.ts +0 -414
- package/tests/commands/unread.test.ts +0 -492
- package/tests/index.test.ts +0 -40
- package/tests/test-utils.ts +0 -28
- package/tests/utils/channel-resolver.test.ts +0 -161
- package/tests/utils/config/config-file-manager.test.ts +0 -118
- package/tests/utils/config/profile-manager.test.ts +0 -266
- package/tests/utils/config/token-crypto-service.test.ts +0 -98
- package/tests/utils/config.test.ts +0 -400
- package/tests/utils/date-utils.test.ts +0 -30
- package/tests/utils/error-utils.test.ts +0 -34
- package/tests/utils/format-utils.test.ts +0 -61
- package/tests/utils/mention-utils.test.ts +0 -100
- package/tests/utils/option-parsers.test.ts +0 -173
- package/tests/utils/profile-config.test.ts +0 -282
- package/tests/utils/schedule-utils.test.ts +0 -63
- package/tests/utils/slack-api-client.test.ts +0 -313
- package/tests/utils/slack-operations/channel-operations.test.ts +0 -248
- package/tests/utils/slack-operations/message-operations.test.ts +0 -163
- package/tests/utils/token-utils.test.ts +0 -33
- package/tests/utils/validators.test.ts +0 -307
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -27
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import { SlackApiClient } from '../../src/utils/slack-api-client';
|
|
3
|
-
import { WebClient, LogLevel } from '@slack/web-api';
|
|
4
|
-
|
|
5
|
-
vi.mock('@slack/web-api');
|
|
6
|
-
|
|
7
|
-
describe('SlackApiClient', () => {
|
|
8
|
-
let client: SlackApiClient;
|
|
9
|
-
let mockWebClient: any;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
vi.clearAllMocks();
|
|
13
|
-
mockWebClient = {
|
|
14
|
-
chat: {
|
|
15
|
-
postMessage: vi.fn(),
|
|
16
|
-
scheduleMessage: vi.fn(),
|
|
17
|
-
scheduledMessages: {
|
|
18
|
-
list: vi.fn(),
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
conversations: {
|
|
22
|
-
list: vi.fn(),
|
|
23
|
-
info: vi.fn(),
|
|
24
|
-
},
|
|
25
|
-
users: {
|
|
26
|
-
conversations: vi.fn(),
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
vi.mocked(WebClient).mockReturnValue(mockWebClient);
|
|
30
|
-
client = new SlackApiClient('test-token');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('constructor', () => {
|
|
34
|
-
it('should create WebClient with provided token', () => {
|
|
35
|
-
expect(WebClient).toHaveBeenCalledWith('test-token', {
|
|
36
|
-
retryConfig: {
|
|
37
|
-
retries: 0, // Disabled to handle rate limits manually
|
|
38
|
-
},
|
|
39
|
-
logLevel: LogLevel.ERROR,
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe('sendMessage', () => {
|
|
45
|
-
it('should send message to channel', async () => {
|
|
46
|
-
const mockResponse = { ok: true, ts: '1234567890.123456' };
|
|
47
|
-
vi.mocked(mockWebClient.chat.postMessage).mockResolvedValue(mockResponse as any);
|
|
48
|
-
|
|
49
|
-
const result = await client.sendMessage('general', 'Hello, World!');
|
|
50
|
-
|
|
51
|
-
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
|
|
52
|
-
channel: 'general',
|
|
53
|
-
text: 'Hello, World!',
|
|
54
|
-
});
|
|
55
|
-
expect(result).toEqual(mockResponse);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should handle channel ID format', async () => {
|
|
59
|
-
const mockResponse = { ok: true, ts: '1234567890.123456' };
|
|
60
|
-
vi.mocked(mockWebClient.chat.postMessage).mockResolvedValue(mockResponse as any);
|
|
61
|
-
|
|
62
|
-
await client.sendMessage('C1234567890', 'Hello!');
|
|
63
|
-
|
|
64
|
-
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
|
|
65
|
-
channel: 'C1234567890',
|
|
66
|
-
text: 'Hello!',
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should handle multi-line messages', async () => {
|
|
71
|
-
const mockResponse = { ok: true, ts: '1234567890.123456' };
|
|
72
|
-
vi.mocked(mockWebClient.chat.postMessage).mockResolvedValue(mockResponse as any);
|
|
73
|
-
|
|
74
|
-
const multiLineMessage = 'Line 1\nLine 2\nLine 3';
|
|
75
|
-
await client.sendMessage('general', multiLineMessage);
|
|
76
|
-
|
|
77
|
-
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
|
|
78
|
-
channel: 'general',
|
|
79
|
-
text: multiLineMessage,
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should throw error on API failure', async () => {
|
|
84
|
-
const mockError = new Error('channel_not_found');
|
|
85
|
-
vi.mocked(mockWebClient.chat.postMessage).mockRejectedValue(mockError);
|
|
86
|
-
|
|
87
|
-
await expect(client.sendMessage('nonexistent', 'Hello')).rejects.toThrow('channel_not_found');
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
describe('scheduleMessage', () => {
|
|
92
|
-
it('should schedule message to channel', async () => {
|
|
93
|
-
const mockResponse = { ok: true, scheduled_message_id: 'Q123', post_at: 1770855000 };
|
|
94
|
-
vi.mocked(mockWebClient.chat.scheduleMessage).mockResolvedValue(mockResponse as any);
|
|
95
|
-
|
|
96
|
-
const result = await client.scheduleMessage('general', 'Hello, future!', 1770855000);
|
|
97
|
-
|
|
98
|
-
expect(mockWebClient.chat.scheduleMessage).toHaveBeenCalledWith({
|
|
99
|
-
channel: 'general',
|
|
100
|
-
text: 'Hello, future!',
|
|
101
|
-
post_at: 1770855000,
|
|
102
|
-
});
|
|
103
|
-
expect(result).toEqual(mockResponse);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('listScheduledMessages', () => {
|
|
108
|
-
it('should list scheduled messages', async () => {
|
|
109
|
-
const mockResponse = {
|
|
110
|
-
ok: true,
|
|
111
|
-
scheduled_messages: [
|
|
112
|
-
{ id: 'Q123', channel_id: 'C123', post_at: 1770855000, date_created: 1770854400 },
|
|
113
|
-
],
|
|
114
|
-
};
|
|
115
|
-
vi.mocked(mockWebClient.chat.scheduledMessages.list).mockResolvedValue(mockResponse as any);
|
|
116
|
-
|
|
117
|
-
const result = await client.listScheduledMessages();
|
|
118
|
-
|
|
119
|
-
expect(mockWebClient.chat.scheduledMessages.list).toHaveBeenCalledWith({ limit: 50 });
|
|
120
|
-
expect(result).toEqual(mockResponse.scheduled_messages);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('listChannels', () => {
|
|
125
|
-
it('should list channels with default options', async () => {
|
|
126
|
-
const mockChannels = [
|
|
127
|
-
{
|
|
128
|
-
id: 'C1234567890',
|
|
129
|
-
name: 'general',
|
|
130
|
-
is_channel: true,
|
|
131
|
-
is_private: false,
|
|
132
|
-
num_members: 150,
|
|
133
|
-
created: 1579075200,
|
|
134
|
-
purpose: { value: 'Company announcements' },
|
|
135
|
-
},
|
|
136
|
-
];
|
|
137
|
-
vi.mocked(mockWebClient.conversations.list).mockResolvedValue({
|
|
138
|
-
ok: true,
|
|
139
|
-
channels: mockChannels,
|
|
140
|
-
} as any);
|
|
141
|
-
|
|
142
|
-
const result = await client.listChannels({
|
|
143
|
-
types: 'public_channel',
|
|
144
|
-
exclude_archived: true,
|
|
145
|
-
limit: 100,
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
expect(mockWebClient.conversations.list).toHaveBeenCalledWith({
|
|
149
|
-
types: 'public_channel',
|
|
150
|
-
exclude_archived: true,
|
|
151
|
-
limit: 100,
|
|
152
|
-
});
|
|
153
|
-
expect(result).toEqual(mockChannels);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('should handle private channels', async () => {
|
|
157
|
-
const mockChannels = [
|
|
158
|
-
{
|
|
159
|
-
id: 'G1234567890',
|
|
160
|
-
name: 'private-channel',
|
|
161
|
-
is_group: true,
|
|
162
|
-
is_private: true,
|
|
163
|
-
num_members: 10,
|
|
164
|
-
created: 1579075200,
|
|
165
|
-
purpose: { value: 'Private discussions' },
|
|
166
|
-
},
|
|
167
|
-
];
|
|
168
|
-
vi.mocked(mockWebClient.conversations.list).mockResolvedValue({
|
|
169
|
-
ok: true,
|
|
170
|
-
channels: mockChannels,
|
|
171
|
-
} as any);
|
|
172
|
-
|
|
173
|
-
const result = await client.listChannels({
|
|
174
|
-
types: 'private_channel',
|
|
175
|
-
exclude_archived: true,
|
|
176
|
-
limit: 50,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
expect(mockWebClient.conversations.list).toHaveBeenCalledWith({
|
|
180
|
-
types: 'private_channel',
|
|
181
|
-
exclude_archived: true,
|
|
182
|
-
limit: 50,
|
|
183
|
-
});
|
|
184
|
-
expect(result).toEqual(mockChannels);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('should handle multiple channel types', async () => {
|
|
188
|
-
vi.mocked(mockWebClient.conversations.list).mockResolvedValue({
|
|
189
|
-
ok: true,
|
|
190
|
-
channels: [],
|
|
191
|
-
} as any);
|
|
192
|
-
|
|
193
|
-
await client.listChannels({
|
|
194
|
-
types: 'public_channel,private_channel,im',
|
|
195
|
-
exclude_archived: false,
|
|
196
|
-
limit: 200,
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
expect(mockWebClient.conversations.list).toHaveBeenCalledWith({
|
|
200
|
-
types: 'public_channel,private_channel,im',
|
|
201
|
-
exclude_archived: false,
|
|
202
|
-
limit: 200,
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should handle API errors', async () => {
|
|
207
|
-
vi.mocked(mockWebClient.conversations.list).mockRejectedValue(new Error('invalid_auth'));
|
|
208
|
-
|
|
209
|
-
await expect(
|
|
210
|
-
client.listChannels({
|
|
211
|
-
types: 'public_channel',
|
|
212
|
-
exclude_archived: true,
|
|
213
|
-
limit: 100,
|
|
214
|
-
})
|
|
215
|
-
).rejects.toThrow('invalid_auth');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('should handle pagination when listing channels', async () => {
|
|
219
|
-
// First page
|
|
220
|
-
vi.mocked(mockWebClient.conversations.list).mockResolvedValueOnce({
|
|
221
|
-
ok: true,
|
|
222
|
-
channels: [
|
|
223
|
-
{ id: 'C001', name: 'channel1', is_private: false },
|
|
224
|
-
{ id: 'C002', name: 'channel2', is_private: false },
|
|
225
|
-
],
|
|
226
|
-
response_metadata: {
|
|
227
|
-
next_cursor: 'cursor123',
|
|
228
|
-
},
|
|
229
|
-
} as any);
|
|
230
|
-
|
|
231
|
-
// Second page
|
|
232
|
-
vi.mocked(mockWebClient.conversations.list).mockResolvedValueOnce({
|
|
233
|
-
ok: true,
|
|
234
|
-
channels: [
|
|
235
|
-
{ id: 'C003', name: 'channel3', is_private: false },
|
|
236
|
-
{ id: 'C004', name: 'channel4', is_private: false },
|
|
237
|
-
],
|
|
238
|
-
response_metadata: {
|
|
239
|
-
next_cursor: 'cursor456',
|
|
240
|
-
},
|
|
241
|
-
} as any);
|
|
242
|
-
|
|
243
|
-
// Third page (last page)
|
|
244
|
-
vi.mocked(mockWebClient.conversations.list).mockResolvedValueOnce({
|
|
245
|
-
ok: true,
|
|
246
|
-
channels: [{ id: 'C005', name: 'channel5', is_private: false }],
|
|
247
|
-
response_metadata: {
|
|
248
|
-
next_cursor: '',
|
|
249
|
-
},
|
|
250
|
-
} as any);
|
|
251
|
-
|
|
252
|
-
const result = await client.listChannels({
|
|
253
|
-
types: 'public_channel',
|
|
254
|
-
exclude_archived: true,
|
|
255
|
-
limit: 2,
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
// Should have called the API 3 times
|
|
259
|
-
expect(mockWebClient.conversations.list).toHaveBeenCalledTimes(3);
|
|
260
|
-
|
|
261
|
-
// First call
|
|
262
|
-
expect(mockWebClient.conversations.list).toHaveBeenNthCalledWith(1, {
|
|
263
|
-
types: 'public_channel',
|
|
264
|
-
exclude_archived: true,
|
|
265
|
-
limit: 2,
|
|
266
|
-
cursor: undefined,
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// Second call
|
|
270
|
-
expect(mockWebClient.conversations.list).toHaveBeenNthCalledWith(2, {
|
|
271
|
-
types: 'public_channel',
|
|
272
|
-
exclude_archived: true,
|
|
273
|
-
limit: 2,
|
|
274
|
-
cursor: 'cursor123',
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// Third call
|
|
278
|
-
expect(mockWebClient.conversations.list).toHaveBeenNthCalledWith(3, {
|
|
279
|
-
types: 'public_channel',
|
|
280
|
-
exclude_archived: true,
|
|
281
|
-
limit: 2,
|
|
282
|
-
cursor: 'cursor456',
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
// Should return all channels
|
|
286
|
-
expect(result).toHaveLength(5);
|
|
287
|
-
expect(result[0].name).toBe('channel1');
|
|
288
|
-
expect(result[1].name).toBe('channel2');
|
|
289
|
-
expect(result[2].name).toBe('channel3');
|
|
290
|
-
expect(result[3].name).toBe('channel4');
|
|
291
|
-
expect(result[4].name).toBe('channel5');
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
it('should handle empty cursor in pagination', async () => {
|
|
295
|
-
vi.mocked(mockWebClient.conversations.list).mockResolvedValueOnce({
|
|
296
|
-
ok: true,
|
|
297
|
-
channels: [{ id: 'C001', name: 'channel1', is_private: false }],
|
|
298
|
-
response_metadata: {
|
|
299
|
-
// No next_cursor field
|
|
300
|
-
},
|
|
301
|
-
} as any);
|
|
302
|
-
|
|
303
|
-
const result = await client.listChannels({
|
|
304
|
-
types: 'public_channel',
|
|
305
|
-
exclude_archived: true,
|
|
306
|
-
limit: 100,
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
expect(mockWebClient.conversations.list).toHaveBeenCalledTimes(1);
|
|
310
|
-
expect(result).toHaveLength(1);
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
});
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { ChannelOperations } from '../../../src/utils/slack-operations/channel-operations';
|
|
3
|
-
|
|
4
|
-
vi.mock('@slack/web-api', () => ({
|
|
5
|
-
WebClient: vi.fn().mockImplementation(() => ({
|
|
6
|
-
conversations: {
|
|
7
|
-
list: vi.fn(),
|
|
8
|
-
info: vi.fn(),
|
|
9
|
-
history: vi.fn(),
|
|
10
|
-
},
|
|
11
|
-
})),
|
|
12
|
-
LogLevel: {
|
|
13
|
-
ERROR: 'error',
|
|
14
|
-
},
|
|
15
|
-
}));
|
|
16
|
-
|
|
17
|
-
vi.mock('p-limit', () => ({
|
|
18
|
-
default: () => (fn: any) => fn(),
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
describe('ChannelOperations', () => {
|
|
22
|
-
let channelOps: ChannelOperations;
|
|
23
|
-
let mockClient: any;
|
|
24
|
-
|
|
25
|
-
beforeEach(() => {
|
|
26
|
-
vi.clearAllMocks();
|
|
27
|
-
mockClient = {
|
|
28
|
-
conversations: {
|
|
29
|
-
list: vi.fn(),
|
|
30
|
-
info: vi.fn(),
|
|
31
|
-
history: vi.fn(),
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
// Create instance with mocked token
|
|
35
|
-
channelOps = new ChannelOperations('test-token');
|
|
36
|
-
// Replace the client with our mock
|
|
37
|
-
(channelOps as any).client = mockClient;
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('listUnreadChannels', () => {
|
|
41
|
-
it('should detect unread messages when last_read is present', async () => {
|
|
42
|
-
// Mock conversations.list response
|
|
43
|
-
mockClient.conversations.list.mockResolvedValue({
|
|
44
|
-
channels: [
|
|
45
|
-
{ id: 'C123', name: 'general' },
|
|
46
|
-
{ id: 'C456', name: 'random' },
|
|
47
|
-
],
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Mock conversations.info responses
|
|
51
|
-
mockClient.conversations.info
|
|
52
|
-
.mockResolvedValueOnce({
|
|
53
|
-
channel: {
|
|
54
|
-
id: 'C123',
|
|
55
|
-
name: 'general',
|
|
56
|
-
last_read: '1234567890.000100',
|
|
57
|
-
},
|
|
58
|
-
})
|
|
59
|
-
.mockResolvedValueOnce({
|
|
60
|
-
channel: {
|
|
61
|
-
id: 'C456',
|
|
62
|
-
name: 'random',
|
|
63
|
-
last_read: '1234567890.000200',
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Mock conversations.history responses
|
|
68
|
-
mockClient.conversations.history
|
|
69
|
-
// First call for C123 - check latest message
|
|
70
|
-
.mockResolvedValueOnce({
|
|
71
|
-
messages: [{ ts: '1234567890.000200' }], // Newer than last_read
|
|
72
|
-
})
|
|
73
|
-
// Second call for C123 - get unread messages
|
|
74
|
-
.mockResolvedValueOnce({
|
|
75
|
-
messages: [
|
|
76
|
-
{ ts: '1234567890.000200' },
|
|
77
|
-
{ ts: '1234567890.000150' },
|
|
78
|
-
],
|
|
79
|
-
})
|
|
80
|
-
// Third call for C456 - check latest message
|
|
81
|
-
.mockResolvedValueOnce({
|
|
82
|
-
messages: [{ ts: '1234567890.000100' }], // Older than last_read
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const result = await channelOps.listUnreadChannels();
|
|
86
|
-
|
|
87
|
-
expect(result).toHaveLength(1);
|
|
88
|
-
expect(result[0]).toMatchObject({
|
|
89
|
-
id: 'C123',
|
|
90
|
-
name: 'general',
|
|
91
|
-
unread_count: 2,
|
|
92
|
-
unread_count_display: 2,
|
|
93
|
-
last_read: '1234567890.000100',
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should count all messages as unread when last_read is not present', async () => {
|
|
98
|
-
// Mock conversations.list response
|
|
99
|
-
mockClient.conversations.list.mockResolvedValue({
|
|
100
|
-
channels: [{ id: 'C789', name: 'no-read-channel' }],
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// Mock conversations.info response - no last_read
|
|
104
|
-
mockClient.conversations.info.mockResolvedValue({
|
|
105
|
-
channel: {
|
|
106
|
-
id: 'C789',
|
|
107
|
-
name: 'no-read-channel',
|
|
108
|
-
// last_read is missing
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Mock conversations.history responses
|
|
113
|
-
mockClient.conversations.history
|
|
114
|
-
// First call - check if channel has messages
|
|
115
|
-
.mockResolvedValueOnce({
|
|
116
|
-
messages: [{ ts: '1234567890.000300' }],
|
|
117
|
-
})
|
|
118
|
-
// Second call - get all messages (up to 100)
|
|
119
|
-
.mockResolvedValueOnce({
|
|
120
|
-
messages: [
|
|
121
|
-
{ ts: '1234567890.000300' },
|
|
122
|
-
{ ts: '1234567890.000200' },
|
|
123
|
-
{ ts: '1234567890.000100' },
|
|
124
|
-
],
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const result = await channelOps.listUnreadChannels();
|
|
128
|
-
|
|
129
|
-
expect(result).toHaveLength(1);
|
|
130
|
-
expect(result[0]).toMatchObject({
|
|
131
|
-
id: 'C789',
|
|
132
|
-
name: 'no-read-channel',
|
|
133
|
-
unread_count: 3,
|
|
134
|
-
unread_count_display: 3,
|
|
135
|
-
last_read: undefined,
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should skip channels with no messages', async () => {
|
|
140
|
-
mockClient.conversations.list.mockResolvedValue({
|
|
141
|
-
channels: [{ id: 'C999', name: 'empty-channel' }],
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
mockClient.conversations.info.mockResolvedValue({
|
|
145
|
-
channel: {
|
|
146
|
-
id: 'C999',
|
|
147
|
-
name: 'empty-channel',
|
|
148
|
-
last_read: '1234567890.000100',
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// No messages in channel
|
|
153
|
-
mockClient.conversations.history.mockResolvedValue({
|
|
154
|
-
messages: [],
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const result = await channelOps.listUnreadChannels();
|
|
158
|
-
|
|
159
|
-
expect(result).toHaveLength(0);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('should skip channels with all messages read', async () => {
|
|
163
|
-
mockClient.conversations.list.mockResolvedValue({
|
|
164
|
-
channels: [{ id: 'C111', name: 'all-read' }],
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
mockClient.conversations.info.mockResolvedValue({
|
|
168
|
-
channel: {
|
|
169
|
-
id: 'C111',
|
|
170
|
-
name: 'all-read',
|
|
171
|
-
last_read: '1234567890.000200',
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
// First call to get latest message (limit: 1)
|
|
176
|
-
mockClient.conversations.history.mockResolvedValueOnce({
|
|
177
|
-
messages: [{ ts: '1234567890.000100' }],
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
// Second call to get messages after last_read (should be empty)
|
|
181
|
-
mockClient.conversations.history.mockResolvedValueOnce({
|
|
182
|
-
messages: [],
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
const result = await channelOps.listUnreadChannels();
|
|
186
|
-
|
|
187
|
-
expect(result).toHaveLength(0);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('should handle API errors gracefully', async () => {
|
|
191
|
-
mockClient.conversations.list.mockResolvedValue({
|
|
192
|
-
channels: [
|
|
193
|
-
{ id: 'C222', name: 'error-channel' },
|
|
194
|
-
{ id: 'C333', name: 'good-channel' },
|
|
195
|
-
],
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// First channel throws error
|
|
199
|
-
mockClient.conversations.info
|
|
200
|
-
.mockRejectedValueOnce(new Error('API Error'))
|
|
201
|
-
.mockResolvedValueOnce({
|
|
202
|
-
channel: {
|
|
203
|
-
id: 'C333',
|
|
204
|
-
name: 'good-channel',
|
|
205
|
-
last_read: '1234567890.000100',
|
|
206
|
-
},
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
mockClient.conversations.history
|
|
210
|
-
.mockResolvedValueOnce({
|
|
211
|
-
messages: [{ ts: '1234567890.000200' }],
|
|
212
|
-
})
|
|
213
|
-
.mockResolvedValueOnce({
|
|
214
|
-
messages: [{ ts: '1234567890.000200' }],
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const result = await channelOps.listUnreadChannels();
|
|
218
|
-
|
|
219
|
-
// Should skip the error channel and process the good one
|
|
220
|
-
expect(result).toHaveLength(1);
|
|
221
|
-
expect(result[0].id).toBe('C333');
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('should handle rate limiting with delay', async () => {
|
|
225
|
-
const delaySpy = vi.spyOn(channelOps as any, 'delay');
|
|
226
|
-
|
|
227
|
-
mockClient.conversations.list.mockResolvedValue({
|
|
228
|
-
channels: [
|
|
229
|
-
{ id: 'C444', name: 'channel1' },
|
|
230
|
-
{ id: 'C555', name: 'channel2' },
|
|
231
|
-
],
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
mockClient.conversations.info.mockResolvedValue({
|
|
235
|
-
channel: { id: 'C444', name: 'channel1' },
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
mockClient.conversations.history.mockResolvedValue({
|
|
239
|
-
messages: [],
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
await channelOps.listUnreadChannels();
|
|
243
|
-
|
|
244
|
-
// Verify delay was called between API calls
|
|
245
|
-
expect(delaySpy).toHaveBeenCalledWith(100);
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
});
|