@urugus/slack-cli 0.1.5 → 0.1.6
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 +31 -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 +1 -0
- package/dist/utils/slack-api-client.d.ts.map +1 -1
- package/dist/utils/slack-api-client.js +59 -101
- 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 +73 -106
- 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 +8 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { WebClient, ChatPostMessageResponse } 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,14 @@ 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: RATE_LIMIT.RETRY_CONFIG,
|
|
86
|
+
});
|
|
87
|
+
// Limit concurrent API calls to avoid rate limiting
|
|
88
|
+
this.rateLimiter = pLimit(RATE_LIMIT.CONCURRENT_REQUESTS);
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
async sendMessage(channel: string, text: string): Promise<ChatPostMessageResponse> {
|
|
@@ -104,48 +119,14 @@ export class SlackApiClient {
|
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
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({
|
|
122
|
+
// Resolve channel name to ID if needed
|
|
123
|
+
const channelId = await channelResolver.resolveChannelId(channel, () =>
|
|
124
|
+
this.listChannels({
|
|
112
125
|
types: 'public_channel,private_channel,im,mpim',
|
|
113
126
|
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
|
-
}
|
|
127
|
+
limit: DEFAULTS.CHANNELS_LIMIT,
|
|
128
|
+
})
|
|
129
|
+
);
|
|
149
130
|
|
|
150
131
|
const response = await this.client.conversations.history({
|
|
151
132
|
channel: channelId,
|
|
@@ -187,84 +168,70 @@ export class SlackApiClient {
|
|
|
187
168
|
|
|
188
169
|
const channels = response.channels as Channel[];
|
|
189
170
|
|
|
190
|
-
//
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
171
|
+
// Batch process channels to reduce rate limit issues
|
|
172
|
+
const batchSize = RATE_LIMIT.BATCH_SIZE;
|
|
173
|
+
const batches: Channel[][] = [];
|
|
174
|
+
for (let i = 0; i < channels.length; i += batchSize) {
|
|
175
|
+
batches.push(channels.slice(i, i + batchSize));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const channelsWithUnread: Channel[] = [];
|
|
179
|
+
|
|
180
|
+
// Process batches sequentially with delay
|
|
181
|
+
for (const batch of batches) {
|
|
182
|
+
const batchResults = await Promise.all(
|
|
183
|
+
batch.map((channel) =>
|
|
184
|
+
this.rateLimiter(async () => {
|
|
185
|
+
try {
|
|
186
|
+
const info = await this.client.conversations.info({
|
|
187
|
+
channel: channel.id,
|
|
188
|
+
include_num_members: false,
|
|
189
|
+
});
|
|
190
|
+
const channelInfo = info.channel as ChannelWithUnreadInfo;
|
|
191
|
+
return {
|
|
192
|
+
...channel,
|
|
193
|
+
unread_count: channelInfo.unread_count || 0,
|
|
194
|
+
unread_count_display: channelInfo.unread_count_display || 0,
|
|
195
|
+
last_read: channelInfo.last_read,
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
// Log rate limit errors but continue processing
|
|
199
|
+
if (error instanceof Error && error.message?.includes('rate limit')) {
|
|
200
|
+
console.warn(`Rate limit hit for channel ${channel.name}, skipping...`);
|
|
201
|
+
}
|
|
202
|
+
return channel;
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
channelsWithUnread.push(...batchResults);
|
|
209
|
+
|
|
210
|
+
// Add delay between batches to avoid rate limiting
|
|
211
|
+
if (batches.indexOf(batch) < batches.length - 1) {
|
|
212
|
+
await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT.BATCH_DELAY_MS));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
210
215
|
|
|
211
216
|
// Filter to only channels with unread messages
|
|
212
217
|
return channelsWithUnread.filter((channel) => (channel.unread_count_display || 0) > 0);
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
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({
|
|
221
|
+
// Resolve channel name to ID if needed
|
|
222
|
+
const channelId = await channelResolver.resolveChannelId(channelNameOrId, () =>
|
|
223
|
+
this.listChannels({
|
|
225
224
|
types: 'public_channel,private_channel,im,mpim',
|
|
226
225
|
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
|
-
}
|
|
226
|
+
limit: DEFAULTS.CHANNELS_LIMIT,
|
|
227
|
+
})
|
|
228
|
+
);
|
|
262
229
|
|
|
263
230
|
// Get channel info with unread count
|
|
264
231
|
const info = await this.client.conversations.info({
|
|
265
232
|
channel: channelId,
|
|
266
233
|
});
|
|
267
|
-
const channel = info.channel as
|
|
234
|
+
const channel = info.channel as ChannelWithUnreadInfo;
|
|
268
235
|
|
|
269
236
|
// Get unread messages
|
|
270
237
|
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
|
+
});
|
|
@@ -24,7 +24,14 @@ describe('SlackApiClient', () => {
|
|
|
24
24
|
|
|
25
25
|
describe('constructor', () => {
|
|
26
26
|
it('should create WebClient with provided token', () => {
|
|
27
|
-
expect(WebClient).toHaveBeenCalledWith('test-token'
|
|
27
|
+
expect(WebClient).toHaveBeenCalledWith('test-token', {
|
|
28
|
+
retryConfig: {
|
|
29
|
+
retries: 3,
|
|
30
|
+
factor: 2,
|
|
31
|
+
minTimeout: 1000,
|
|
32
|
+
maxTimeout: 30000,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
28
35
|
});
|
|
29
36
|
});
|
|
30
37
|
|