@urugus/slack-cli 0.1.8 → 0.2.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/.claude/settings.local.json +2 -1
- package/dist/utils/slack-operations/channel-operations.d.ts +0 -1
- package/dist/utils/slack-operations/channel-operations.d.ts.map +1 -1
- package/dist/utils/slack-operations/channel-operations.js +42 -26
- package/dist/utils/slack-operations/channel-operations.js.map +1 -1
- package/package.json +1 -1
- package/src/utils/slack-operations/channel-operations.ts +45 -28
- package/tests/utils/slack-operations/channel-operations.test.ts +243 -0
|
@@ -8,7 +8,6 @@ interface ChannelWithUnreadInfo extends Channel {
|
|
|
8
8
|
export declare class ChannelOperations extends BaseSlackClient {
|
|
9
9
|
listChannels(options: ListChannelsOptions): Promise<Channel[]>;
|
|
10
10
|
listUnreadChannels(): Promise<Channel[]>;
|
|
11
|
-
private listUnreadChannelsFallback;
|
|
12
11
|
getChannelInfo(channelNameOrId: string): Promise<ChannelWithUnreadInfo>;
|
|
13
12
|
}
|
|
14
13
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-operations.d.ts","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,UAAU,qBAAsB,SAAQ,OAAO;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAkB,SAAQ,eAAe;IAC9C,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAuB9D,kBAAkB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"channel-operations.d.ts","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAEnE,UAAU,qBAAsB,SAAQ,OAAO;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,iBAAkB,SAAQ,eAAe;IAC9C,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAuB9D,kBAAkB,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;IA8ExC,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAe9E"}
|
|
@@ -24,25 +24,6 @@ class ChannelOperations extends base_client_1.BaseSlackClient {
|
|
|
24
24
|
return channels;
|
|
25
25
|
}
|
|
26
26
|
async listUnreadChannels() {
|
|
27
|
-
try {
|
|
28
|
-
// Use users.conversations to get unread counts in a single API call
|
|
29
|
-
const response = await this.client.users.conversations({
|
|
30
|
-
types: 'public_channel,private_channel,im,mpim',
|
|
31
|
-
exclude_archived: true,
|
|
32
|
-
limit: 1000,
|
|
33
|
-
});
|
|
34
|
-
const channels = response.channels;
|
|
35
|
-
// Filter to only channels with unread messages
|
|
36
|
-
// The users.conversations endpoint includes unread_count_display
|
|
37
|
-
return channels.filter((channel) => (channel.unread_count_display || 0) > 0);
|
|
38
|
-
}
|
|
39
|
-
catch (error) {
|
|
40
|
-
// Fallback to the old method if users.conversations fails
|
|
41
|
-
console.warn('Failed to use users.conversations, falling back to conversations.list');
|
|
42
|
-
return this.listUnreadChannelsFallback();
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
async listUnreadChannelsFallback() {
|
|
46
27
|
// Get all conversations the user is a member of
|
|
47
28
|
const response = await this.client.conversations.list({
|
|
48
29
|
types: 'public_channel,private_channel,im,mpim',
|
|
@@ -59,13 +40,48 @@ class ChannelOperations extends base_client_1.BaseSlackClient {
|
|
|
59
40
|
include_num_members: false,
|
|
60
41
|
});
|
|
61
42
|
const channelInfo = info.channel;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
// Get the latest message in the channel
|
|
44
|
+
const history = await this.client.conversations.history({
|
|
45
|
+
channel: channel.id,
|
|
46
|
+
limit: 1,
|
|
47
|
+
});
|
|
48
|
+
if (history.messages && history.messages.length > 0) {
|
|
49
|
+
let hasUnread = false;
|
|
50
|
+
let unreadCount = 0;
|
|
51
|
+
if (!channelInfo.last_read) {
|
|
52
|
+
// If last_read is empty, all messages are unread
|
|
53
|
+
hasUnread = true;
|
|
54
|
+
// Get total message count (up to 100)
|
|
55
|
+
const allHistory = await this.client.conversations.history({
|
|
56
|
+
channel: channel.id,
|
|
57
|
+
limit: 100,
|
|
58
|
+
});
|
|
59
|
+
unreadCount = allHistory.messages?.length || 0;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
// Check if there are messages after last_read
|
|
63
|
+
const latestMessage = history.messages[0];
|
|
64
|
+
const lastReadTs = parseFloat(channelInfo.last_read);
|
|
65
|
+
const latestMessageTs = parseFloat(latestMessage.ts || '0');
|
|
66
|
+
if (latestMessageTs > lastReadTs) {
|
|
67
|
+
hasUnread = true;
|
|
68
|
+
// Calculate unread count by fetching messages after last_read
|
|
69
|
+
const unreadHistory = await this.client.conversations.history({
|
|
70
|
+
channel: channel.id,
|
|
71
|
+
oldest: channelInfo.last_read,
|
|
72
|
+
limit: 100, // Get up to 100 unread messages
|
|
73
|
+
});
|
|
74
|
+
unreadCount = unreadHistory.messages?.length || 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (hasUnread) {
|
|
78
|
+
channelsWithUnread.push({
|
|
79
|
+
...channel,
|
|
80
|
+
unread_count: unreadCount,
|
|
81
|
+
unread_count_display: unreadCount,
|
|
82
|
+
last_read: channelInfo.last_read,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
69
85
|
}
|
|
70
86
|
// Add delay between API calls to avoid rate limiting
|
|
71
87
|
await this.delay(100);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":";;;AAAA,+CAAgD;AAChD,0DAAsD;AACtD,4CAAwC;AASxC,MAAa,iBAAkB,SAAQ,6BAAe;IACpD,KAAK,CAAC,YAAY,CAAC,OAA4B;QAC7C,MAAM,QAAQ,GAAc,EAAE,CAAC;QAC/B,IAAI,MAA0B,CAAC;QAE/B,gCAAgC;QAChC,GAAG,CAAC;YACF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC;gBACpD,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,gBAAgB,EAAE,OAAO,CAAC,gBAAgB;gBAC1C,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,MAAM;aACP,CAAC,CAAC;YAEH,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;gBACtB,QAAQ,CAAC,IAAI,CAAC,GAAI,QAAQ,CAAC,QAAsB,CAAC,CAAC;YACrD,CAAC;YAED,MAAM,GAAG,QAAQ,CAAC,iBAAiB,EAAE,WAAW,CAAC;QACnD,CAAC,QAAQ,MAAM,EAAE;QAEjB,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,
|
|
1
|
+
{"version":3,"file":"channel-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -34,27 +34,6 @@ export class ChannelOperations extends BaseSlackClient {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
async listUnreadChannels(): Promise<Channel[]> {
|
|
37
|
-
try {
|
|
38
|
-
// Use users.conversations to get unread counts in a single API call
|
|
39
|
-
const response = await this.client.users.conversations({
|
|
40
|
-
types: 'public_channel,private_channel,im,mpim',
|
|
41
|
-
exclude_archived: true,
|
|
42
|
-
limit: 1000,
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
const channels = response.channels as Channel[];
|
|
46
|
-
|
|
47
|
-
// Filter to only channels with unread messages
|
|
48
|
-
// The users.conversations endpoint includes unread_count_display
|
|
49
|
-
return channels.filter((channel) => (channel.unread_count_display || 0) > 0);
|
|
50
|
-
} catch (error) {
|
|
51
|
-
// Fallback to the old method if users.conversations fails
|
|
52
|
-
console.warn('Failed to use users.conversations, falling back to conversations.list');
|
|
53
|
-
return this.listUnreadChannelsFallback();
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private async listUnreadChannelsFallback(): Promise<Channel[]> {
|
|
58
37
|
// Get all conversations the user is a member of
|
|
59
38
|
const response = await this.client.conversations.list({
|
|
60
39
|
types: 'public_channel,private_channel,im,mpim',
|
|
@@ -74,13 +53,51 @@ export class ChannelOperations extends BaseSlackClient {
|
|
|
74
53
|
});
|
|
75
54
|
const channelInfo = info.channel as ChannelWithUnreadInfo;
|
|
76
55
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
56
|
+
// Get the latest message in the channel
|
|
57
|
+
const history = await this.client.conversations.history({
|
|
58
|
+
channel: channel.id,
|
|
59
|
+
limit: 1,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (history.messages && history.messages.length > 0) {
|
|
63
|
+
let hasUnread = false;
|
|
64
|
+
let unreadCount = 0;
|
|
65
|
+
|
|
66
|
+
if (!channelInfo.last_read) {
|
|
67
|
+
// If last_read is empty, all messages are unread
|
|
68
|
+
hasUnread = true;
|
|
69
|
+
// Get total message count (up to 100)
|
|
70
|
+
const allHistory = await this.client.conversations.history({
|
|
71
|
+
channel: channel.id,
|
|
72
|
+
limit: 100,
|
|
73
|
+
});
|
|
74
|
+
unreadCount = allHistory.messages?.length || 0;
|
|
75
|
+
} else {
|
|
76
|
+
// Check if there are messages after last_read
|
|
77
|
+
const latestMessage = history.messages[0];
|
|
78
|
+
const lastReadTs = parseFloat(channelInfo.last_read!);
|
|
79
|
+
const latestMessageTs = parseFloat(latestMessage.ts || '0');
|
|
80
|
+
|
|
81
|
+
if (latestMessageTs > lastReadTs) {
|
|
82
|
+
hasUnread = true;
|
|
83
|
+
// Calculate unread count by fetching messages after last_read
|
|
84
|
+
const unreadHistory = await this.client.conversations.history({
|
|
85
|
+
channel: channel.id,
|
|
86
|
+
oldest: channelInfo.last_read,
|
|
87
|
+
limit: 100, // Get up to 100 unread messages
|
|
88
|
+
});
|
|
89
|
+
unreadCount = unreadHistory.messages?.length || 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (hasUnread) {
|
|
94
|
+
channelsWithUnread.push({
|
|
95
|
+
...channel,
|
|
96
|
+
unread_count: unreadCount,
|
|
97
|
+
unread_count_display: unreadCount,
|
|
98
|
+
last_read: channelInfo.last_read,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
84
101
|
}
|
|
85
102
|
|
|
86
103
|
// Add delay between API calls to avoid rate limiting
|
|
@@ -0,0 +1,243 @@
|
|
|
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
|
+
// Latest message is older than last_read
|
|
176
|
+
mockClient.conversations.history.mockResolvedValue({
|
|
177
|
+
messages: [{ ts: '1234567890.000100' }],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const result = await channelOps.listUnreadChannels();
|
|
181
|
+
|
|
182
|
+
expect(result).toHaveLength(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should handle API errors gracefully', async () => {
|
|
186
|
+
mockClient.conversations.list.mockResolvedValue({
|
|
187
|
+
channels: [
|
|
188
|
+
{ id: 'C222', name: 'error-channel' },
|
|
189
|
+
{ id: 'C333', name: 'good-channel' },
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// First channel throws error
|
|
194
|
+
mockClient.conversations.info
|
|
195
|
+
.mockRejectedValueOnce(new Error('API Error'))
|
|
196
|
+
.mockResolvedValueOnce({
|
|
197
|
+
channel: {
|
|
198
|
+
id: 'C333',
|
|
199
|
+
name: 'good-channel',
|
|
200
|
+
last_read: '1234567890.000100',
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
mockClient.conversations.history
|
|
205
|
+
.mockResolvedValueOnce({
|
|
206
|
+
messages: [{ ts: '1234567890.000200' }],
|
|
207
|
+
})
|
|
208
|
+
.mockResolvedValueOnce({
|
|
209
|
+
messages: [{ ts: '1234567890.000200' }],
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const result = await channelOps.listUnreadChannels();
|
|
213
|
+
|
|
214
|
+
// Should skip the error channel and process the good one
|
|
215
|
+
expect(result).toHaveLength(1);
|
|
216
|
+
expect(result[0].id).toBe('C333');
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should handle rate limiting with delay', async () => {
|
|
220
|
+
const delaySpy = vi.spyOn(channelOps as any, 'delay');
|
|
221
|
+
|
|
222
|
+
mockClient.conversations.list.mockResolvedValue({
|
|
223
|
+
channels: [
|
|
224
|
+
{ id: 'C444', name: 'channel1' },
|
|
225
|
+
{ id: 'C555', name: 'channel2' },
|
|
226
|
+
],
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
mockClient.conversations.info.mockResolvedValue({
|
|
230
|
+
channel: { id: 'C444', name: 'channel1' },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
mockClient.conversations.history.mockResolvedValue({
|
|
234
|
+
messages: [],
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
await channelOps.listUnreadChannels();
|
|
238
|
+
|
|
239
|
+
// Verify delay was called between API calls
|
|
240
|
+
expect(delaySpy).toHaveBeenCalledWith(100);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|