@urugus/slack-cli 0.1.5 → 0.1.7
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/.claude/settings.local.json +2 -1
- package/CHANGELOG.md +39 -0
- package/dist/commands/unread.d.ts.map +1 -1
- package/dist/commands/unread.js +6 -49
- package/dist/commands/unread.js.map +1 -1
- package/dist/utils/channel-resolver.d.ts +26 -0
- package/dist/utils/channel-resolver.d.ts.map +1 -0
- package/dist/utils/channel-resolver.js +74 -0
- package/dist/utils/channel-resolver.js.map +1 -0
- package/dist/utils/constants.d.ts +17 -0
- package/dist/utils/constants.d.ts.map +1 -1
- package/dist/utils/constants.js +21 -1
- package/dist/utils/constants.js.map +1 -1
- package/dist/utils/formatters/channel-formatters.d.ts +16 -0
- package/dist/utils/formatters/channel-formatters.d.ts.map +1 -0
- package/dist/utils/formatters/channel-formatters.js +73 -0
- package/dist/utils/formatters/channel-formatters.js.map +1 -0
- package/dist/utils/formatters/output-formatter.d.ts +7 -0
- package/dist/utils/formatters/output-formatter.d.ts.map +1 -0
- package/dist/utils/formatters/output-formatter.js +7 -0
- package/dist/utils/formatters/output-formatter.js.map +1 -0
- package/dist/utils/profile-config.d.ts.map +1 -1
- package/dist/utils/profile-config.js +5 -3
- package/dist/utils/profile-config.js.map +1 -1
- package/dist/utils/slack-api-client.d.ts +2 -0
- package/dist/utils/slack-api-client.d.ts.map +1 -1
- package/dist/utils/slack-api-client.js +67 -96
- package/dist/utils/slack-api-client.js.map +1 -1
- package/package.json +5 -4
- package/src/commands/unread.ts +11 -52
- package/src/utils/channel-resolver.ts +86 -0
- package/src/utils/constants.ts +23 -0
- package/src/utils/formatters/channel-formatters.ts +69 -0
- package/src/utils/formatters/output-formatter.ts +7 -0
- package/src/utils/profile-config.ts +7 -5
- package/src/utils/slack-api-client.ts +81 -105
- package/tests/commands/unread.test.ts +31 -0
- package/tests/utils/channel-resolver.test.ts +157 -0
- package/tests/utils/slack-api-client.test.ts +12 -3
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { WebClient, ChatPostMessageResponse } from '@slack/web-api';
|
|
1
|
+
import { WebClient, ChatPostMessageResponse, LogLevel } from '@slack/web-api';
|
|
2
|
+
import pLimit from 'p-limit';
|
|
3
|
+
import { channelResolver } from './channel-resolver';
|
|
4
|
+
import { RATE_LIMIT, DEFAULTS } from './constants';
|
|
2
5
|
|
|
3
6
|
export interface Channel {
|
|
4
7
|
id: string;
|
|
@@ -33,6 +36,13 @@ export interface Channel {
|
|
|
33
36
|
};
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
// Extended channel interface with additional properties from API
|
|
40
|
+
interface ChannelWithUnreadInfo extends Channel {
|
|
41
|
+
unread_count: number;
|
|
42
|
+
unread_count_display: number;
|
|
43
|
+
last_read?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
export interface ListChannelsOptions {
|
|
37
47
|
types: string;
|
|
38
48
|
exclude_archived: boolean;
|
|
@@ -68,9 +78,17 @@ export interface ChannelUnreadResult {
|
|
|
68
78
|
|
|
69
79
|
export class SlackApiClient {
|
|
70
80
|
private client: WebClient;
|
|
81
|
+
private rateLimiter: ReturnType<typeof pLimit>;
|
|
71
82
|
|
|
72
83
|
constructor(token: string) {
|
|
73
|
-
this.client = new WebClient(token
|
|
84
|
+
this.client = new WebClient(token, {
|
|
85
|
+
retryConfig: {
|
|
86
|
+
retries: 0, // Disable automatic retries to handle rate limits manually
|
|
87
|
+
},
|
|
88
|
+
logLevel: LogLevel.ERROR, // Reduce noise from WebClient logs
|
|
89
|
+
});
|
|
90
|
+
// Limit concurrent API calls to avoid rate limiting
|
|
91
|
+
this.rateLimiter = pLimit(RATE_LIMIT.CONCURRENT_REQUESTS);
|
|
74
92
|
}
|
|
75
93
|
|
|
76
94
|
async sendMessage(channel: string, text: string): Promise<ChatPostMessageResponse> {
|
|
@@ -104,48 +122,14 @@ export class SlackApiClient {
|
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
async getHistory(channel: string, options: HistoryOptions): Promise<HistoryResult> {
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// It's a name, not an ID - need to find the ID
|
|
111
|
-
const channels = await this.listChannels({
|
|
125
|
+
// Resolve channel name to ID if needed
|
|
126
|
+
const channelId = await channelResolver.resolveChannelId(channel, () =>
|
|
127
|
+
this.listChannels({
|
|
112
128
|
types: 'public_channel,private_channel,im,mpim',
|
|
113
129
|
exclude_archived: true,
|
|
114
|
-
limit:
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
// Try multiple matching strategies
|
|
118
|
-
const foundChannel = channels.find((c) => {
|
|
119
|
-
// Direct name match
|
|
120
|
-
if (c.name === channel) return true;
|
|
121
|
-
// Match without # prefix
|
|
122
|
-
if (c.name === channel.replace('#', '')) return true;
|
|
123
|
-
// Case-insensitive match
|
|
124
|
-
if (c.name?.toLowerCase() === channel.toLowerCase()) return true;
|
|
125
|
-
// Match with normalized name
|
|
126
|
-
if (c.name_normalized === channel) return true;
|
|
127
|
-
return false;
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
if (!foundChannel) {
|
|
131
|
-
// Provide helpful error message
|
|
132
|
-
const similarChannels = channels
|
|
133
|
-
.filter((c) => c.name?.toLowerCase().includes(channel.toLowerCase()))
|
|
134
|
-
.slice(0, 5)
|
|
135
|
-
.map((c) => c.name);
|
|
136
|
-
|
|
137
|
-
if (similarChannels.length > 0) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
`Channel '${channel}' not found. Did you mean one of these? ${similarChannels.join(', ')}`
|
|
140
|
-
);
|
|
141
|
-
} else {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`Channel '${channel}' not found. Make sure you are a member of this channel.`
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
channelId = foundChannel.id;
|
|
148
|
-
}
|
|
130
|
+
limit: DEFAULTS.CHANNELS_LIMIT,
|
|
131
|
+
})
|
|
132
|
+
);
|
|
149
133
|
|
|
150
134
|
const response = await this.client.conversations.history({
|
|
151
135
|
channel: channelId,
|
|
@@ -178,6 +162,28 @@ export class SlackApiClient {
|
|
|
178
162
|
}
|
|
179
163
|
|
|
180
164
|
async listUnreadChannels(): Promise<Channel[]> {
|
|
165
|
+
try {
|
|
166
|
+
// Use users.conversations to get unread counts in a single API call
|
|
167
|
+
const response = await this.client.users.conversations({
|
|
168
|
+
types: 'public_channel,private_channel,im,mpim',
|
|
169
|
+
exclude_archived: true,
|
|
170
|
+
limit: 1000,
|
|
171
|
+
user: undefined, // Current authenticated user
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const channels = response.channels as Channel[];
|
|
175
|
+
|
|
176
|
+
// Filter to only channels with unread messages
|
|
177
|
+
// The users.conversations endpoint includes unread_count_display
|
|
178
|
+
return channels.filter((channel) => (channel.unread_count_display || 0) > 0);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// Fallback to the old method if users.conversations fails
|
|
181
|
+
console.warn('Failed to use users.conversations, falling back to conversations.list');
|
|
182
|
+
return this.listUnreadChannelsFallback();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async listUnreadChannelsFallback(): Promise<Channel[]> {
|
|
181
187
|
// Get all conversations the user is a member of
|
|
182
188
|
const response = await this.client.conversations.list({
|
|
183
189
|
types: 'public_channel,private_channel,im,mpim',
|
|
@@ -186,85 +192,55 @@ export class SlackApiClient {
|
|
|
186
192
|
});
|
|
187
193
|
|
|
188
194
|
const channels = response.channels as Channel[];
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
195
|
+
const channelsWithUnread: Channel[] = [];
|
|
196
|
+
|
|
197
|
+
// Process channels one by one with delay to avoid rate limits
|
|
198
|
+
for (const channel of channels) {
|
|
199
|
+
try {
|
|
200
|
+
const info = await this.client.conversations.info({
|
|
201
|
+
channel: channel.id,
|
|
202
|
+
include_num_members: false,
|
|
203
|
+
});
|
|
204
|
+
const channelInfo = info.channel as ChannelWithUnreadInfo;
|
|
205
|
+
|
|
206
|
+
if (channelInfo.unread_count_display && channelInfo.unread_count_display > 0) {
|
|
207
|
+
channelsWithUnread.push({
|
|
200
208
|
...channel,
|
|
201
209
|
unread_count: channelInfo.unread_count || 0,
|
|
202
210
|
unread_count_display: channelInfo.unread_count_display || 0,
|
|
203
211
|
last_read: channelInfo.last_read,
|
|
204
|
-
};
|
|
205
|
-
} catch {
|
|
206
|
-
return channel;
|
|
212
|
+
});
|
|
207
213
|
}
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
|
|
215
|
+
// Add delay between API calls to avoid rate limiting
|
|
216
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// Skip channels that fail
|
|
219
|
+
if (error instanceof Error && error.message?.includes('rate limit')) {
|
|
220
|
+
// If we hit rate limit, wait longer
|
|
221
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
210
225
|
|
|
211
|
-
|
|
212
|
-
return channelsWithUnread.filter((channel) => (channel.unread_count_display || 0) > 0);
|
|
226
|
+
return channelsWithUnread;
|
|
213
227
|
}
|
|
214
228
|
|
|
215
229
|
async getChannelUnread(channelNameOrId: string): Promise<ChannelUnreadResult> {
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
!channelNameOrId.startsWith('C') &&
|
|
220
|
-
!channelNameOrId.startsWith('D') &&
|
|
221
|
-
!channelNameOrId.startsWith('G')
|
|
222
|
-
) {
|
|
223
|
-
// It's a name, not an ID - need to find the ID
|
|
224
|
-
const channels = await this.listChannels({
|
|
230
|
+
// Resolve channel name to ID if needed
|
|
231
|
+
const channelId = await channelResolver.resolveChannelId(channelNameOrId, () =>
|
|
232
|
+
this.listChannels({
|
|
225
233
|
types: 'public_channel,private_channel,im,mpim',
|
|
226
234
|
exclude_archived: true,
|
|
227
|
-
limit:
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
// Try multiple matching strategies (same as getHistory)
|
|
231
|
-
const channel = channels.find((c) => {
|
|
232
|
-
// Direct name match
|
|
233
|
-
if (c.name === channelNameOrId) return true;
|
|
234
|
-
// Match without # prefix
|
|
235
|
-
if (c.name === channelNameOrId.replace('#', '')) return true;
|
|
236
|
-
// Case-insensitive match
|
|
237
|
-
if (c.name?.toLowerCase() === channelNameOrId.toLowerCase()) return true;
|
|
238
|
-
// Match with normalized name
|
|
239
|
-
if (c.name_normalized === channelNameOrId) return true;
|
|
240
|
-
return false;
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
if (!channel) {
|
|
244
|
-
// Provide helpful error message
|
|
245
|
-
const similarChannels = channels
|
|
246
|
-
.filter((c) => c.name?.toLowerCase().includes(channelNameOrId.toLowerCase()))
|
|
247
|
-
.slice(0, 5)
|
|
248
|
-
.map((c) => c.name);
|
|
249
|
-
|
|
250
|
-
if (similarChannels.length > 0) {
|
|
251
|
-
throw new Error(
|
|
252
|
-
`Channel '${channelNameOrId}' not found. Did you mean one of these? ${similarChannels.join(', ')}`
|
|
253
|
-
);
|
|
254
|
-
} else {
|
|
255
|
-
throw new Error(
|
|
256
|
-
`Channel '${channelNameOrId}' not found. Make sure you are a member of this channel.`
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
channelId = channel.id;
|
|
261
|
-
}
|
|
235
|
+
limit: DEFAULTS.CHANNELS_LIMIT,
|
|
236
|
+
})
|
|
237
|
+
);
|
|
262
238
|
|
|
263
239
|
// Get channel info with unread count
|
|
264
240
|
const info = await this.client.conversations.info({
|
|
265
241
|
channel: channelId,
|
|
266
242
|
});
|
|
267
|
-
const channel = info.channel as
|
|
243
|
+
const channel = info.channel as ChannelWithUnreadInfo;
|
|
268
244
|
|
|
269
245
|
// Get unread messages
|
|
270
246
|
let messages: Message[] = [];
|
|
@@ -245,4 +245,35 @@ describe('unread command', () => {
|
|
|
245
245
|
expect(SlackApiClient).toHaveBeenCalledWith('work-token');
|
|
246
246
|
});
|
|
247
247
|
});
|
|
248
|
+
|
|
249
|
+
describe('rate limiting', () => {
|
|
250
|
+
it('should handle rate limit errors gracefully', async () => {
|
|
251
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
252
|
+
token: 'test-token',
|
|
253
|
+
updatedAt: new Date().toISOString()
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const rateLimitError = new Error('A rate limit was exceeded (url: conversations.info, retry-after: 10)');
|
|
257
|
+
vi.mocked(mockSlackClient.listUnreadChannels).mockRejectedValue(rateLimitError);
|
|
258
|
+
|
|
259
|
+
await program.parseAsync(['node', 'slack-cli', 'unread']);
|
|
260
|
+
|
|
261
|
+
expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.stringContaining('rate limit'));
|
|
262
|
+
expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('should not retry indefinitely on rate limit errors', async () => {
|
|
266
|
+
vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
|
|
267
|
+
token: 'test-token',
|
|
268
|
+
updatedAt: new Date().toISOString()
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const rateLimitError = new Error('A rate limit was exceeded (url: conversations.info, retry-after: 10)');
|
|
272
|
+
vi.mocked(mockSlackClient.listUnreadChannels).mockRejectedValue(rateLimitError);
|
|
273
|
+
|
|
274
|
+
await program.parseAsync(['node', 'slack-cli', 'unread']);
|
|
275
|
+
|
|
276
|
+
expect(mockSlackClient.listUnreadChannels).toHaveBeenCalledTimes(1);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
248
279
|
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { ChannelResolver } from '../../src/utils/channel-resolver';
|
|
3
|
+
import { Channel } from '../../src/utils/slack-api-client';
|
|
4
|
+
|
|
5
|
+
describe('ChannelResolver', () => {
|
|
6
|
+
let resolver: ChannelResolver;
|
|
7
|
+
let mockChannels: Channel[];
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
resolver = new ChannelResolver();
|
|
11
|
+
mockChannels = [
|
|
12
|
+
{
|
|
13
|
+
id: 'C123',
|
|
14
|
+
name: 'general',
|
|
15
|
+
is_private: false,
|
|
16
|
+
created: 1234567890,
|
|
17
|
+
is_member: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'C456',
|
|
21
|
+
name: 'random',
|
|
22
|
+
is_private: false,
|
|
23
|
+
created: 1234567890,
|
|
24
|
+
is_member: true,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'C789',
|
|
28
|
+
name: 'dev-team',
|
|
29
|
+
is_private: false,
|
|
30
|
+
created: 1234567890,
|
|
31
|
+
is_member: true,
|
|
32
|
+
name_normalized: 'dev-team',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'G123',
|
|
36
|
+
name: 'private-channel',
|
|
37
|
+
is_private: true,
|
|
38
|
+
created: 1234567890,
|
|
39
|
+
is_member: true,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('isChannelId', () => {
|
|
45
|
+
it('should identify channel IDs starting with C', () => {
|
|
46
|
+
expect(resolver.isChannelId('C123456')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should identify DM IDs starting with D', () => {
|
|
50
|
+
expect(resolver.isChannelId('D123456')).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should identify group IDs starting with G', () => {
|
|
54
|
+
expect(resolver.isChannelId('G123456')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return false for channel names', () => {
|
|
58
|
+
expect(resolver.isChannelId('general')).toBe(false);
|
|
59
|
+
expect(resolver.isChannelId('#general')).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('findChannel', () => {
|
|
64
|
+
it('should find channel by exact name match', () => {
|
|
65
|
+
const result = resolver.findChannel('general', mockChannels);
|
|
66
|
+
expect(result).toEqual(mockChannels[0]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should find channel by name without # prefix', () => {
|
|
70
|
+
const result = resolver.findChannel('#general', mockChannels);
|
|
71
|
+
expect(result).toEqual(mockChannels[0]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should find channel by case-insensitive match', () => {
|
|
75
|
+
const result = resolver.findChannel('GENERAL', mockChannels);
|
|
76
|
+
expect(result).toEqual(mockChannels[0]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should find channel by normalized name', () => {
|
|
80
|
+
const result = resolver.findChannel('dev-team', mockChannels);
|
|
81
|
+
expect(result).toEqual(mockChannels[2]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should return undefined when channel not found', () => {
|
|
85
|
+
const result = resolver.findChannel('nonexistent', mockChannels);
|
|
86
|
+
expect(result).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('getSimilarChannels', () => {
|
|
91
|
+
it('should find similar channels by partial match', () => {
|
|
92
|
+
const result = resolver.getSimilarChannels('gen', mockChannels);
|
|
93
|
+
expect(result).toEqual(['general']);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should limit results to specified count', () => {
|
|
97
|
+
const manyChannels = [
|
|
98
|
+
...mockChannels,
|
|
99
|
+
{ id: 'C999', name: 'general-2', is_private: false, created: 0 },
|
|
100
|
+
{ id: 'C888', name: 'general-3', is_private: false, created: 0 },
|
|
101
|
+
];
|
|
102
|
+
const result = resolver.getSimilarChannels('general', manyChannels, 2);
|
|
103
|
+
expect(result).toHaveLength(2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should return empty array when no similar channels found', () => {
|
|
107
|
+
const result = resolver.getSimilarChannels('xyz', mockChannels);
|
|
108
|
+
expect(result).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('resolveChannelError', () => {
|
|
113
|
+
it('should create error with suggestions when similar channels exist', () => {
|
|
114
|
+
const error = resolver.resolveChannelError('genera', mockChannels);
|
|
115
|
+
expect(error.message).toContain("Channel 'genera' not found");
|
|
116
|
+
expect(error.message).toContain('Did you mean one of these?');
|
|
117
|
+
expect(error.message).toContain('general');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should create error without suggestions when no similar channels', () => {
|
|
121
|
+
const error = resolver.resolveChannelError('xyz', mockChannels);
|
|
122
|
+
expect(error.message).toContain("Channel 'xyz' not found");
|
|
123
|
+
expect(error.message).toContain('Make sure you are a member of this channel');
|
|
124
|
+
expect(error.message).not.toContain('Did you mean');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('resolveChannelId', () => {
|
|
129
|
+
it('should return ID directly if already an ID', async () => {
|
|
130
|
+
const getChannelsFn = vi.fn();
|
|
131
|
+
const result = await resolver.resolveChannelId('C123456', getChannelsFn);
|
|
132
|
+
expect(result).toBe('C123456');
|
|
133
|
+
expect(getChannelsFn).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should resolve channel name to ID', async () => {
|
|
137
|
+
const getChannelsFn = vi.fn().mockResolvedValue(mockChannels);
|
|
138
|
+
const result = await resolver.resolveChannelId('general', getChannelsFn);
|
|
139
|
+
expect(result).toBe('C123');
|
|
140
|
+
expect(getChannelsFn).toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should throw error when channel not found', async () => {
|
|
144
|
+
const getChannelsFn = vi.fn().mockResolvedValue(mockChannels);
|
|
145
|
+
await expect(resolver.resolveChannelId('nonexistent', getChannelsFn)).rejects.toThrow(
|
|
146
|
+
"Channel 'nonexistent' not found"
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should include suggestions in error when similar channels exist', async () => {
|
|
151
|
+
const getChannelsFn = vi.fn().mockResolvedValue(mockChannels);
|
|
152
|
+
await expect(resolver.resolveChannelId('genera', getChannelsFn)).rejects.toThrow(
|
|
153
|
+
'Did you mean one of these? general'
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
2
|
import { SlackApiClient } from '../../src/utils/slack-api-client';
|
|
3
|
-
import { WebClient } from '@slack/web-api';
|
|
3
|
+
import { WebClient, LogLevel } from '@slack/web-api';
|
|
4
4
|
|
|
5
5
|
vi.mock('@slack/web-api');
|
|
6
6
|
|
|
@@ -15,7 +15,11 @@ describe('SlackApiClient', () => {
|
|
|
15
15
|
postMessage: vi.fn()
|
|
16
16
|
},
|
|
17
17
|
conversations: {
|
|
18
|
-
list: vi.fn()
|
|
18
|
+
list: vi.fn(),
|
|
19
|
+
info: vi.fn()
|
|
20
|
+
},
|
|
21
|
+
users: {
|
|
22
|
+
conversations: vi.fn()
|
|
19
23
|
}
|
|
20
24
|
};
|
|
21
25
|
vi.mocked(WebClient).mockReturnValue(mockWebClient);
|
|
@@ -24,7 +28,12 @@ describe('SlackApiClient', () => {
|
|
|
24
28
|
|
|
25
29
|
describe('constructor', () => {
|
|
26
30
|
it('should create WebClient with provided token', () => {
|
|
27
|
-
expect(WebClient).toHaveBeenCalledWith('test-token'
|
|
31
|
+
expect(WebClient).toHaveBeenCalledWith('test-token', {
|
|
32
|
+
retryConfig: {
|
|
33
|
+
retries: 0, // Disabled to handle rate limits manually
|
|
34
|
+
},
|
|
35
|
+
logLevel: LogLevel.ERROR,
|
|
36
|
+
});
|
|
28
37
|
});
|
|
29
38
|
});
|
|
30
39
|
|