@urugus/slack-cli 0.1.9 → 0.2.1

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.
@@ -55,7 +55,8 @@
55
55
  "Bash(rg:*)",
56
56
  "Bash(npm version:*)",
57
57
  "Bash(npx tsx:*)",
58
- "Bash(gh workflow view:*)"
58
+ "Bash(gh workflow view:*)",
59
+ "Bash(git tag:*)"
59
60
  ],
60
61
  "deny": []
61
62
  }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.2.1] - 2025-06-23
6
+
7
+ ### Fixed
8
+ - Fixed unread message detection for channels where unread_count is 0 but messages exist after last_read timestamp
9
+ - Always check messages after last_read timestamp for accurate unread count
10
+ - Improved reliability of unread message detection for channels like dev_kiban_jira
11
+
12
+ ## [0.2.0] - 2025-06-23
13
+
14
+ ### Changed
15
+ - Major version bump for improved unread message detection
16
+
17
+ ## [0.1.9] - 2025-06-22
18
+
19
+ ### Fixed
20
+ - Improved unread message detection using last_read timestamp
21
+
22
+ ## [0.1.8] - 2025-06-22
23
+
24
+ ### Changed
25
+ - Refactored code organization with separation of concerns
26
+
5
27
  ## [0.1.7] - 2025-06-22
6
28
 
7
29
  ### Fixed
@@ -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;YAahC,0BAA0B;IAwClC,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAe9E"}
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;IAyExC,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;CAe9E"}
@@ -24,19 +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
- // This endpoint doesn't return unread_count_display by default,
30
- // so we'll use the fallback method instead
31
- throw new Error('Using fallback method for unread counts');
32
- }
33
- catch (error) {
34
- // Fallback to the old method if users.conversations fails
35
- console.warn('Failed to use users.conversations, falling back to conversations.list');
36
- return this.listUnreadChannelsFallback();
37
- }
38
- }
39
- async listUnreadChannelsFallback() {
40
27
  // Get all conversations the user is a member of
41
28
  const response = await this.client.conversations.list({
42
29
  types: 'public_channel,private_channel,im,mpim',
@@ -53,13 +40,44 @@ class ChannelOperations extends base_client_1.BaseSlackClient {
53
40
  include_num_members: false,
54
41
  });
55
42
  const channelInfo = info.channel;
56
- if (channelInfo.unread_count_display && channelInfo.unread_count_display > 0) {
57
- channelsWithUnread.push({
58
- ...channel,
59
- unread_count: channelInfo.unread_count || 0,
60
- unread_count_display: channelInfo.unread_count_display || 0,
61
- last_read: channelInfo.last_read,
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
+ // Always check for messages after last_read timestamp
49
+ if (channelInfo.last_read) {
50
+ // Fetch messages after last_read
51
+ const unreadHistory = await this.client.conversations.history({
52
+ channel: channel.id,
53
+ oldest: channelInfo.last_read,
54
+ limit: 100, // Get up to 100 unread messages
55
+ });
56
+ const unreadCount = unreadHistory.messages?.length || 0;
57
+ if (unreadCount > 0) {
58
+ channelsWithUnread.push({
59
+ ...channel,
60
+ unread_count: unreadCount,
61
+ unread_count_display: unreadCount,
62
+ last_read: channelInfo.last_read,
63
+ });
64
+ }
65
+ }
66
+ else if (history.messages && history.messages.length > 0) {
67
+ // If no last_read, all messages are unread
68
+ const allHistory = await this.client.conversations.history({
69
+ channel: channel.id,
70
+ limit: 100,
62
71
  });
72
+ const unreadCount = allHistory.messages?.length || 0;
73
+ if (unreadCount > 0) {
74
+ channelsWithUnread.push({
75
+ ...channel,
76
+ unread_count: unreadCount,
77
+ unread_count_display: unreadCount,
78
+ last_read: channelInfo.last_read,
79
+ });
80
+ }
63
81
  }
64
82
  // Add delay between API calls to avoid rate limiting
65
83
  await this.delay(100);
@@ -1 +1 @@
1
- {"version":3,"file":"channel-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"channel-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/channel-operations.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"message-operations.d.ts","sourceRoot":"","sources":["../../../src/utils/slack-operations/message-operations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAW,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAGlG,qBAAa,iBAAkB,SAAQ,eAAe;IACpD,OAAO,CAAC,UAAU,CAAoB;gBAE1B,KAAK,EAAE,MAAM;IAKnB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAO5E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAyB5E,gBAAgB,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;YA2B/D,aAAa;CAmB5B"}
1
+ {"version":3,"file":"message-operations.d.ts","sourceRoot":"","sources":["../../../src/utils/slack-operations/message-operations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,EAAW,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAGlG,qBAAa,iBAAkB,SAAQ,eAAe;IACpD,OAAO,CAAC,UAAU,CAAoB;gBAE1B,KAAK,EAAE,MAAM;IAKnB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAO5E,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IAyB5E,gBAAgB,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;YAsC/D,aAAa;CAmB5B"}
@@ -39,19 +39,31 @@ class MessageOperations extends base_client_1.BaseSlackClient {
39
39
  // Get unread messages
40
40
  let messages = [];
41
41
  let users = new Map();
42
- if (channel.last_read && channel.unread_count > 0) {
42
+ let actualUnreadCount = 0;
43
+ if (channel.last_read) {
44
+ // Always fetch messages after last_read to get accurate unread count
43
45
  const historyResult = await this.getHistory(channel.id, {
44
- limit: channel.unread_count,
46
+ limit: 100, // Fetch up to 100 messages after last_read
45
47
  oldest: channel.last_read,
46
48
  });
47
49
  messages = historyResult.messages;
48
50
  users = historyResult.users;
51
+ actualUnreadCount = messages.length;
52
+ }
53
+ else if (!channel.last_read) {
54
+ // If no last_read, all messages are unread
55
+ const historyResult = await this.getHistory(channel.id, {
56
+ limit: 100,
57
+ });
58
+ messages = historyResult.messages;
59
+ users = historyResult.users;
60
+ actualUnreadCount = messages.length;
49
61
  }
50
62
  return {
51
63
  channel: {
52
64
  ...channel,
53
- unread_count: channel.unread_count || 0,
54
- unread_count_display: channel.unread_count_display || 0,
65
+ unread_count: actualUnreadCount,
66
+ unread_count_display: actualUnreadCount,
55
67
  },
56
68
  messages,
57
69
  users,
@@ -1 +1 @@
1
- {"version":3,"file":"message-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/message-operations.ts"],"names":[],"mappings":";;;AACA,+CAAgD;AAChD,0DAAsD;AACtD,4CAAwC;AAExC,6DAAyD;AAEzD,MAAa,iBAAkB,SAAQ,6BAAe;IAGpD,YAAY,KAAa;QACvB,KAAK,CAAC,KAAK,CAAC,CAAC;QACb,IAAI,CAAC,UAAU,GAAG,IAAI,sCAAiB,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,OAAe,EAAE,IAAY;QAC7C,OAAO,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC;YACxC,OAAO;YACP,IAAI;SACL,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,OAAe,EAAE,OAAuB;QACvD,uCAAuC;QACvC,MAAM,SAAS,GAAG,MAAM,kCAAe,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CACrE,IAAI,CAAC,UAAU,CAAC,YAAY,CAAC;YAC3B,KAAK,EAAE,wCAAwC;YAC/C,gBAAgB,EAAE,IAAI;YACtB,KAAK,EAAE,oBAAQ,CAAC,cAAc;SAC/B,CAAC,CACH,CAAC;QAEF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,OAAO,CAAC;YACvD,OAAO,EAAE,SAAS;YAClB,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,MAAM,EAAE,OAAO,CAAC,MAAM;SACvB,CAAC,CAAC;QAEH,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAqB,CAAC;QAEhD,sBAAsB;QACtB,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAK,CAAC,CAAC,CAAC,CAAC;QACjF,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QAEhD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC7B,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,eAAuB;QAC5C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QAEtE,sBAAsB;QACtB,IAAI,QAAQ,GAAc,EAAE,CAAC;QAC7B,IAAI,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;QAEtC,IAAI,OAAO,CAAC,SAAS,IAAI,OAAO,CAAC,YAAY,GAAG,CAAC,EAAE,CAAC;YAClD,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,EAAE;gBACtD,KAAK,EAAE,OAAO,CAAC,YAAY;gBAC3B,MAAM,EAAE,OAAO,CAAC,SAAS;aAC1B,CAAC,CAAC;YACH,QAAQ,GAAG,aAAa,CAAC,QAAQ,CAAC;YAClC,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC;QAC9B,CAAC;QAED,OAAO;YACL,OAAO,EAAE;gBACP,GAAG,OAAO;gBACV,YAAY,EAAE,OAAO,CAAC,YAAY,IAAI,CAAC;gBACvC,oBAAoB,EAAE,OAAO,CAAC,oBAAoB,IAAI,CAAC;aACxD;YACD,QAAQ;YACR,KAAK;SACN,CAAC;IACJ,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,OAAiB;QAC3C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAC;QAExC,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,IAAI,CAAC;oBACH,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;oBAChE,IAAI,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;wBACxB,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACxC,CAAC;gBACH,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,8CAA8C;oBAC9C,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,KAAK,CAAC;IACf,CAAC;CACF;AAtFD,8CAsFC"}
1
+ {"version":3,"file":"message-operations.js","sourceRoot":"","sources":["../../../src/utils/slack-operations/message-operations.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urugus/slack-cli",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "A command-line tool for sending messages to Slack",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -34,19 +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
- // This endpoint doesn't return unread_count_display by default,
40
- // so we'll use the fallback method instead
41
- throw new Error('Using fallback method for unread counts');
42
- } catch (error) {
43
- // Fallback to the old method if users.conversations fails
44
- console.warn('Failed to use users.conversations, falling back to conversations.list');
45
- return this.listUnreadChannelsFallback();
46
- }
47
- }
48
-
49
- private async listUnreadChannelsFallback(): Promise<Channel[]> {
50
37
  // Get all conversations the user is a member of
51
38
  const response = await this.client.conversations.list({
52
39
  types: 'public_channel,private_channel,im,mpim',
@@ -66,13 +53,46 @@ export class ChannelOperations extends BaseSlackClient {
66
53
  });
67
54
  const channelInfo = info.channel as ChannelWithUnreadInfo;
68
55
 
69
- if (channelInfo.unread_count_display && channelInfo.unread_count_display > 0) {
70
- channelsWithUnread.push({
71
- ...channel,
72
- unread_count: channelInfo.unread_count || 0,
73
- unread_count_display: channelInfo.unread_count_display || 0,
74
- last_read: channelInfo.last_read,
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
+ // Always check for messages after last_read timestamp
63
+ if (channelInfo.last_read) {
64
+ // Fetch messages after last_read
65
+ const unreadHistory = await this.client.conversations.history({
66
+ channel: channel.id,
67
+ oldest: channelInfo.last_read,
68
+ limit: 100, // Get up to 100 unread messages
69
+ });
70
+
71
+ const unreadCount = unreadHistory.messages?.length || 0;
72
+ if (unreadCount > 0) {
73
+ channelsWithUnread.push({
74
+ ...channel,
75
+ unread_count: unreadCount,
76
+ unread_count_display: unreadCount,
77
+ last_read: channelInfo.last_read,
78
+ });
79
+ }
80
+ } else if (history.messages && history.messages.length > 0) {
81
+ // If no last_read, all messages are unread
82
+ const allHistory = await this.client.conversations.history({
83
+ channel: channel.id,
84
+ limit: 100,
75
85
  });
86
+ const unreadCount = allHistory.messages?.length || 0;
87
+
88
+ if (unreadCount > 0) {
89
+ channelsWithUnread.push({
90
+ ...channel,
91
+ unread_count: unreadCount,
92
+ unread_count_display: unreadCount,
93
+ last_read: channelInfo.last_read,
94
+ });
95
+ }
76
96
  }
77
97
 
78
98
  // Add delay between API calls to avoid rate limiting
@@ -51,21 +51,32 @@ export class MessageOperations extends BaseSlackClient {
51
51
  // Get unread messages
52
52
  let messages: Message[] = [];
53
53
  let users = new Map<string, string>();
54
+ let actualUnreadCount = 0;
54
55
 
55
- if (channel.last_read && channel.unread_count > 0) {
56
+ if (channel.last_read) {
57
+ // Always fetch messages after last_read to get accurate unread count
56
58
  const historyResult = await this.getHistory(channel.id, {
57
- limit: channel.unread_count,
59
+ limit: 100, // Fetch up to 100 messages after last_read
58
60
  oldest: channel.last_read,
59
61
  });
60
62
  messages = historyResult.messages;
61
63
  users = historyResult.users;
64
+ actualUnreadCount = messages.length;
65
+ } else if (!channel.last_read) {
66
+ // If no last_read, all messages are unread
67
+ const historyResult = await this.getHistory(channel.id, {
68
+ limit: 100,
69
+ });
70
+ messages = historyResult.messages;
71
+ users = historyResult.users;
72
+ actualUnreadCount = messages.length;
62
73
  }
63
74
 
64
75
  return {
65
76
  channel: {
66
77
  ...channel,
67
- unread_count: channel.unread_count || 0,
68
- unread_count_display: channel.unread_count_display || 0,
78
+ unread_count: actualUnreadCount,
79
+ unread_count_display: actualUnreadCount,
69
80
  },
70
81
  messages,
71
82
  users,
@@ -276,4 +276,45 @@ describe('unread command', () => {
276
276
  expect(mockSlackClient.listUnreadChannels).toHaveBeenCalledTimes(1);
277
277
  });
278
278
  });
279
+
280
+ describe('last_read timestamp handling', () => {
281
+ it('should fetch messages after last_read timestamp even when unread_count is 0', async () => {
282
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
283
+ token: 'test-token',
284
+ updatedAt: new Date().toISOString()
285
+ });
286
+
287
+ const channelWithLastRead = {
288
+ id: 'C08JFKGJPPE',
289
+ name: 'dev_kiban_jira',
290
+ is_channel: true,
291
+ is_member: true,
292
+ is_archived: false,
293
+ unread_count: 0,
294
+ unread_count_display: 0,
295
+ last_read: '1750646034.663209',
296
+ is_private: false,
297
+ created: 1742353688
298
+ };
299
+
300
+ const unreadMessage = {
301
+ ts: '1750646072.447069',
302
+ user: 'U5F87BSGP',
303
+ text: '@Suguru Sakashita / 阪下 駿 transitioned ES-4359 ArgumentError: \'発行者\' is not a valid field_name in Clip',
304
+ type: 'message',
305
+ };
306
+
307
+ vi.mocked(mockSlackClient.getChannelUnread).mockResolvedValue({
308
+ channel: { ...channelWithLastRead, unread_count: 1, unread_count_display: 1 },
309
+ messages: [unreadMessage],
310
+ users: new Map([['U5F87BSGP', 'jira-bot']])
311
+ });
312
+
313
+ await program.parseAsync(['node', 'slack-cli', 'unread', '--channel', 'dev_kiban_jira']);
314
+
315
+ expect(mockSlackClient.getChannelUnread).toHaveBeenCalledWith('dev_kiban_jira');
316
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(chalk.bold('#dev_kiban_jira: 1 unread messages'));
317
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining('transitioned ES-4359'));
318
+ });
319
+ });
279
320
  });
@@ -0,0 +1,248 @@
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
+ });