@urugus/slack-cli 0.2.9 → 0.2.11

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.
Files changed (65) hide show
  1. package/.claude/settings.local.json +60 -14
  2. package/README.md +71 -28
  3. package/dist/commands/scheduled.d.ts +3 -0
  4. package/dist/commands/scheduled.d.ts.map +1 -0
  5. package/dist/commands/scheduled.js +55 -0
  6. package/dist/commands/scheduled.js.map +1 -0
  7. package/dist/commands/send.d.ts.map +1 -1
  8. package/dist/commands/send.js +16 -2
  9. package/dist/commands/send.js.map +1 -1
  10. package/dist/index.js +2 -0
  11. package/dist/index.js.map +1 -1
  12. package/dist/types/commands.d.ts +8 -0
  13. package/dist/types/commands.d.ts.map +1 -1
  14. package/dist/utils/channel-resolver.d.ts.map +1 -1
  15. package/dist/utils/channel-resolver.js +1 -3
  16. package/dist/utils/channel-resolver.js.map +1 -1
  17. package/dist/utils/config.d.ts +10 -0
  18. package/dist/utils/config.d.ts.map +1 -0
  19. package/dist/utils/config.js +94 -0
  20. package/dist/utils/config.js.map +1 -0
  21. package/dist/utils/constants.d.ts +5 -0
  22. package/dist/utils/constants.d.ts.map +1 -1
  23. package/dist/utils/constants.js +5 -0
  24. package/dist/utils/constants.js.map +1 -1
  25. package/dist/utils/formatters/output-formatter.d.ts +7 -0
  26. package/dist/utils/formatters/output-formatter.d.ts.map +1 -0
  27. package/dist/utils/formatters/output-formatter.js +7 -0
  28. package/dist/utils/formatters/output-formatter.js.map +1 -0
  29. package/dist/utils/profile-config-refactored.d.ts +20 -0
  30. package/dist/utils/profile-config-refactored.d.ts.map +1 -0
  31. package/dist/utils/profile-config-refactored.js +174 -0
  32. package/dist/utils/profile-config-refactored.js.map +1 -0
  33. package/dist/utils/schedule-utils.d.ts +3 -0
  34. package/dist/utils/schedule-utils.d.ts.map +1 -0
  35. package/dist/utils/schedule-utils.js +34 -0
  36. package/dist/utils/schedule-utils.js.map +1 -0
  37. package/dist/utils/slack-api-client.d.ts +10 -1
  38. package/dist/utils/slack-api-client.d.ts.map +1 -1
  39. package/dist/utils/slack-api-client.js +6 -0
  40. package/dist/utils/slack-api-client.js.map +1 -1
  41. package/dist/utils/slack-operations/message-operations.d.ts +4 -2
  42. package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
  43. package/dist/utils/slack-operations/message-operations.js +28 -0
  44. package/dist/utils/slack-operations/message-operations.js.map +1 -1
  45. package/dist/utils/validators.d.ts +4 -0
  46. package/dist/utils/validators.d.ts.map +1 -1
  47. package/dist/utils/validators.js +31 -0
  48. package/dist/utils/validators.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/commands/scheduled.ts +71 -0
  51. package/src/commands/send.ts +21 -3
  52. package/src/index.ts +2 -0
  53. package/src/types/commands.ts +9 -0
  54. package/src/utils/channel-resolver.ts +1 -5
  55. package/src/utils/constants.ts +7 -0
  56. package/src/utils/schedule-utils.ts +41 -0
  57. package/src/utils/slack-api-client.ts +22 -1
  58. package/src/utils/slack-operations/message-operations.ts +55 -2
  59. package/src/utils/validators.ts +38 -0
  60. package/tests/commands/scheduled.test.ts +131 -0
  61. package/tests/commands/send.test.ts +235 -44
  62. package/tests/utils/channel-resolver.test.ts +25 -21
  63. package/tests/utils/schedule-utils.test.ts +63 -0
  64. package/tests/utils/slack-api-client.test.ts +81 -46
  65. package/tests/utils/slack-operations/message-operations.test.ts +38 -1
@@ -18,13 +18,13 @@ describe('send command', () => {
18
18
 
19
19
  beforeEach(() => {
20
20
  vi.clearAllMocks();
21
-
21
+
22
22
  mockConfigManager = new ProfileConfigManager();
23
23
  vi.mocked(ProfileConfigManager).mockReturnValue(mockConfigManager);
24
-
24
+
25
25
  mockSlackClient = new SlackApiClient('test-token');
26
26
  vi.mocked(SlackApiClient).mockReturnValue(mockSlackClient);
27
-
27
+
28
28
  mockConsole = setupMockConsole();
29
29
  program = createTestProgram();
30
30
  program.addCommand(setupSendCommand());
@@ -38,30 +38,42 @@ describe('send command', () => {
38
38
  it('should send a message to specified channel', async () => {
39
39
  vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
40
40
  token: 'test-token',
41
- updatedAt: new Date().toISOString()
41
+ updatedAt: new Date().toISOString(),
42
42
  });
43
43
  vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
44
44
  ok: true,
45
- ts: '1234567890.123456'
45
+ ts: '1234567890.123456',
46
46
  });
47
47
 
48
48
  await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-m', 'Hello, World!']);
49
49
 
50
50
  expect(mockSlackClient.sendMessage).toHaveBeenCalledWith('general', 'Hello, World!', undefined);
51
- expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general')));
51
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(
52
+ expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general'))
53
+ );
52
54
  });
53
55
 
54
56
  it('should use specified profile', async () => {
55
57
  vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
56
58
  token: 'work-token',
57
- updatedAt: new Date().toISOString()
59
+ updatedAt: new Date().toISOString(),
58
60
  });
59
61
  vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
60
62
  ok: true,
61
- ts: '1234567890.123456'
63
+ ts: '1234567890.123456',
62
64
  });
63
65
 
64
- await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-m', 'Hello', '--profile', 'work']);
66
+ await program.parseAsync([
67
+ 'node',
68
+ 'slack-cli',
69
+ 'send',
70
+ '-c',
71
+ 'general',
72
+ '-m',
73
+ 'Hello',
74
+ '--profile',
75
+ 'work',
76
+ ]);
65
77
 
66
78
  expect(mockConfigManager.getConfig).toHaveBeenCalledWith('work');
67
79
  expect(SlackApiClient).toHaveBeenCalledWith('work-token');
@@ -74,11 +86,11 @@ describe('send command', () => {
74
86
  vi.mocked(fs.readFile).mockResolvedValue(fileContent);
75
87
  vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
76
88
  token: 'test-token',
77
- updatedAt: new Date().toISOString()
89
+ updatedAt: new Date().toISOString(),
78
90
  });
79
91
  vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
80
92
  ok: true,
81
- ts: '1234567890.123456'
93
+ ts: '1234567890.123456',
82
94
  });
83
95
 
84
96
  await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-f', 'message.txt']);
@@ -92,46 +104,80 @@ describe('send command', () => {
92
104
  it('should send a reply to a thread with --thread option', async () => {
93
105
  vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
94
106
  token: 'test-token',
95
- updatedAt: new Date().toISOString()
107
+ updatedAt: new Date().toISOString(),
96
108
  });
97
109
  vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
98
110
  ok: true,
99
- ts: '1234567890.123456'
111
+ ts: '1234567890.123456',
100
112
  });
101
113
 
102
- await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-m', 'Reply to thread', '--thread', '1719207629.000100']);
103
-
104
- expect(mockSlackClient.sendMessage).toHaveBeenCalledWith('general', 'Reply to thread', '1719207629.000100');
105
- expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general')));
114
+ await program.parseAsync([
115
+ 'node',
116
+ 'slack-cli',
117
+ 'send',
118
+ '-c',
119
+ 'general',
120
+ '-m',
121
+ 'Reply to thread',
122
+ '--thread',
123
+ '1719207629.000100',
124
+ ]);
125
+
126
+ expect(mockSlackClient.sendMessage).toHaveBeenCalledWith(
127
+ 'general',
128
+ 'Reply to thread',
129
+ '1719207629.000100'
130
+ );
131
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(
132
+ expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general'))
133
+ );
106
134
  });
107
135
 
108
136
  it('should send a reply to a thread with -t option', async () => {
109
137
  vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
110
138
  token: 'test-token',
111
- updatedAt: new Date().toISOString()
139
+ updatedAt: new Date().toISOString(),
112
140
  });
113
141
  vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
114
142
  ok: true,
115
- ts: '1234567890.123456'
143
+ ts: '1234567890.123456',
116
144
  });
117
145
 
118
- await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-m', 'Reply to thread', '-t', '1719207629.000100']);
119
-
120
- expect(mockSlackClient.sendMessage).toHaveBeenCalledWith('general', 'Reply to thread', '1719207629.000100');
121
- expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general')));
146
+ await program.parseAsync([
147
+ 'node',
148
+ 'slack-cli',
149
+ 'send',
150
+ '-c',
151
+ 'general',
152
+ '-m',
153
+ 'Reply to thread',
154
+ '-t',
155
+ '1719207629.000100',
156
+ ]);
157
+
158
+ expect(mockSlackClient.sendMessage).toHaveBeenCalledWith(
159
+ 'general',
160
+ 'Reply to thread',
161
+ '1719207629.000100'
162
+ );
163
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(
164
+ expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general'))
165
+ );
122
166
  });
123
167
 
124
168
  it('should validate thread timestamp format', async () => {
125
169
  vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
126
170
  token: 'test-token',
127
- updatedAt: new Date().toISOString()
171
+ updatedAt: new Date().toISOString(),
128
172
  });
129
173
 
130
174
  const sendCommand = setupSendCommand();
131
175
  sendCommand.exitOverride();
132
176
 
133
177
  await expect(
134
- sendCommand.parseAsync(['-c', 'general', '-m', 'Reply', '-t', 'invalid-timestamp'], { from: 'user' })
178
+ sendCommand.parseAsync(['-c', 'general', '-m', 'Reply', '-t', 'invalid-timestamp'], {
179
+ from: 'user',
180
+ })
135
181
  ).rejects.toThrow(ERROR_MESSAGES.INVALID_THREAD_TIMESTAMP);
136
182
  });
137
183
 
@@ -140,18 +186,107 @@ describe('send command', () => {
140
186
  vi.mocked(fs.readFile).mockResolvedValue(fileContent);
141
187
  vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
142
188
  token: 'test-token',
143
- updatedAt: new Date().toISOString()
189
+ updatedAt: new Date().toISOString(),
144
190
  });
145
191
  vi.mocked(mockSlackClient.sendMessage).mockResolvedValue({
146
192
  ok: true,
147
- ts: '1234567890.123456'
193
+ ts: '1234567890.123456',
148
194
  });
149
195
 
150
- await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-f', 'reply.txt', '-t', '1719207629.000100']);
196
+ await program.parseAsync([
197
+ 'node',
198
+ 'slack-cli',
199
+ 'send',
200
+ '-c',
201
+ 'general',
202
+ '-f',
203
+ 'reply.txt',
204
+ '-t',
205
+ '1719207629.000100',
206
+ ]);
151
207
 
152
208
  expect(fs.readFile).toHaveBeenCalledWith('reply.txt', 'utf-8');
153
- expect(mockSlackClient.sendMessage).toHaveBeenCalledWith('general', fileContent, '1719207629.000100');
154
- expect(mockConsole.logSpy).toHaveBeenCalledWith(expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general')));
209
+ expect(mockSlackClient.sendMessage).toHaveBeenCalledWith(
210
+ 'general',
211
+ fileContent,
212
+ '1719207629.000100'
213
+ );
214
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(
215
+ expect.stringContaining(SUCCESS_MESSAGES.MESSAGE_SENT('general'))
216
+ );
217
+ });
218
+ });
219
+
220
+ describe('schedule message', () => {
221
+ it('should schedule message with --at', async () => {
222
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
223
+ token: 'test-token',
224
+ updatedAt: new Date().toISOString(),
225
+ });
226
+ vi.mocked(mockSlackClient.scheduleMessage).mockResolvedValue({
227
+ ok: true,
228
+ scheduled_message_id: 'Q1298393284',
229
+ post_at: 2051258400,
230
+ } as any);
231
+
232
+ await program.parseAsync([
233
+ 'node',
234
+ 'slack-cli',
235
+ 'send',
236
+ '-c',
237
+ 'general',
238
+ '-m',
239
+ 'Future message',
240
+ '--at',
241
+ '2035-01-01T10:00:00Z',
242
+ ]);
243
+
244
+ expect(mockSlackClient.scheduleMessage).toHaveBeenCalledWith(
245
+ 'general',
246
+ 'Future message',
247
+ 2051258400,
248
+ undefined
249
+ );
250
+ expect(mockSlackClient.sendMessage).not.toHaveBeenCalled();
251
+ expect(mockConsole.logSpy).toHaveBeenCalledWith(
252
+ expect.stringContaining('Message scheduled to #general')
253
+ );
254
+ });
255
+
256
+ it('should schedule message with --after', async () => {
257
+ vi.useFakeTimers();
258
+ vi.setSystemTime(new Date('2026-02-12T00:00:00Z'));
259
+
260
+ vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
261
+ token: 'test-token',
262
+ updatedAt: new Date().toISOString(),
263
+ });
264
+ vi.mocked(mockSlackClient.scheduleMessage).mockResolvedValue({
265
+ ok: true,
266
+ scheduled_message_id: 'Q1298393284',
267
+ post_at: 1770868800,
268
+ } as any);
269
+
270
+ await program.parseAsync([
271
+ 'node',
272
+ 'slack-cli',
273
+ 'send',
274
+ '-c',
275
+ 'general',
276
+ '-m',
277
+ 'Future message',
278
+ '--after',
279
+ '10',
280
+ ]);
281
+
282
+ expect(mockSlackClient.scheduleMessage).toHaveBeenCalledWith(
283
+ 'general',
284
+ 'Future message',
285
+ 1770855000,
286
+ undefined
287
+ );
288
+
289
+ vi.useRealTimers();
155
290
  });
156
291
  });
157
292
 
@@ -160,9 +295,9 @@ describe('send command', () => {
160
295
  const sendCommand = setupSendCommand();
161
296
  sendCommand.exitOverride();
162
297
 
163
- await expect(
164
- sendCommand.parseAsync(['-c', 'general'], { from: 'user' })
165
- ).rejects.toThrow(`Error: ${ERROR_MESSAGES.NO_MESSAGE_OR_FILE}`);
298
+ await expect(sendCommand.parseAsync(['-c', 'general'], { from: 'user' })).rejects.toThrow(
299
+ `Error: ${ERROR_MESSAGES.NO_MESSAGE_OR_FILE}`
300
+ );
166
301
  });
167
302
 
168
303
  it('should fail when both message and file are provided', async () => {
@@ -170,17 +305,64 @@ describe('send command', () => {
170
305
  sendCommand.exitOverride();
171
306
 
172
307
  await expect(
173
- sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '-f', 'file.txt'], { from: 'user' })
308
+ sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '-f', 'file.txt'], {
309
+ from: 'user',
310
+ })
174
311
  ).rejects.toThrow(`Error: ${ERROR_MESSAGES.BOTH_MESSAGE_AND_FILE}`);
175
312
  });
176
313
 
177
- it('should fail when no channel is provided', async () => {
314
+ it('should fail when both --at and --after are provided', async () => {
315
+ const sendCommand = setupSendCommand();
316
+ sendCommand.exitOverride();
317
+
318
+ await expect(
319
+ sendCommand.parseAsync(
320
+ ['-c', 'general', '-m', 'Hello', '--at', '2030-01-01T10:00:00Z', '--after', '10'],
321
+ {
322
+ from: 'user',
323
+ }
324
+ )
325
+ ).rejects.toThrow(`Error: ${ERROR_MESSAGES.BOTH_SCHEDULE_OPTIONS}`);
326
+ });
327
+
328
+ it('should fail with invalid --at', async () => {
329
+ const sendCommand = setupSendCommand();
330
+ sendCommand.exitOverride();
331
+
332
+ await expect(
333
+ sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '--at', 'invalid-date'], {
334
+ from: 'user',
335
+ })
336
+ ).rejects.toThrow(`Error: ${ERROR_MESSAGES.INVALID_SCHEDULE_AT}`);
337
+ });
338
+
339
+ it('should fail with past --at', async () => {
178
340
  const sendCommand = setupSendCommand();
179
341
  sendCommand.exitOverride();
180
-
342
+
181
343
  await expect(
182
- sendCommand.parseAsync(['-m', 'Hello'], { from: 'user' })
183
- ).rejects.toThrow();
344
+ sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '--at', '1'], {
345
+ from: 'user',
346
+ })
347
+ ).rejects.toThrow(`Error: ${ERROR_MESSAGES.SCHEDULE_TIME_IN_PAST}`);
348
+ });
349
+
350
+ it('should fail with invalid --after', async () => {
351
+ const sendCommand = setupSendCommand();
352
+ sendCommand.exitOverride();
353
+
354
+ await expect(
355
+ sendCommand.parseAsync(['-c', 'general', '-m', 'Hello', '--after', '0'], {
356
+ from: 'user',
357
+ })
358
+ ).rejects.toThrow(`Error: ${ERROR_MESSAGES.INVALID_SCHEDULE_AFTER}`);
359
+ });
360
+
361
+ it('should fail when no channel is provided', async () => {
362
+ const sendCommand = setupSendCommand();
363
+ sendCommand.exitOverride();
364
+
365
+ await expect(sendCommand.parseAsync(['-m', 'Hello'], { from: 'user' })).rejects.toThrow();
184
366
  });
185
367
  });
186
368
 
@@ -190,20 +372,26 @@ describe('send command', () => {
190
372
 
191
373
  await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-m', 'Hello']);
192
374
 
193
- expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
375
+ expect(mockConsole.errorSpy).toHaveBeenCalledWith(
376
+ expect.stringContaining('Error:'),
377
+ expect.any(String)
378
+ );
194
379
  expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
195
380
  });
196
381
 
197
382
  it('should handle Slack API errors', async () => {
198
383
  vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
199
384
  token: 'test-token',
200
- updatedAt: new Date().toISOString()
385
+ updatedAt: new Date().toISOString(),
201
386
  });
202
387
  vi.mocked(mockSlackClient.sendMessage).mockRejectedValue(new Error('channel_not_found'));
203
388
 
204
389
  await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'nonexistent', '-m', 'Hello']);
205
390
 
206
- expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
391
+ expect(mockConsole.errorSpy).toHaveBeenCalledWith(
392
+ expect.stringContaining('Error:'),
393
+ expect.any(String)
394
+ );
207
395
  expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
208
396
  });
209
397
 
@@ -211,13 +399,16 @@ describe('send command', () => {
211
399
  vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT: no such file'));
212
400
  vi.mocked(mockConfigManager.getConfig).mockResolvedValue({
213
401
  token: 'test-token',
214
- updatedAt: new Date().toISOString()
402
+ updatedAt: new Date().toISOString(),
215
403
  });
216
404
 
217
405
  await program.parseAsync(['node', 'slack-cli', 'send', '-c', 'general', '-f', 'nonexistent.txt']);
218
406
 
219
- expect(mockConsole.errorSpy).toHaveBeenCalledWith(expect.stringContaining('Error:'), expect.any(String));
407
+ expect(mockConsole.errorSpy).toHaveBeenCalledWith(
408
+ expect.stringContaining('Error:'),
409
+ expect.any(String)
410
+ );
220
411
  expect(mockConsole.exitSpy).toHaveBeenCalledWith(1);
221
412
  });
222
413
  });
223
- });
414
+ });
@@ -10,21 +10,21 @@ describe('ChannelResolver', () => {
10
10
  resolver = new ChannelResolver();
11
11
  mockChannels = [
12
12
  {
13
- id: 'C123',
13
+ id: 'C1234567890',
14
14
  name: 'general',
15
15
  is_private: false,
16
16
  created: 1234567890,
17
17
  is_member: true,
18
18
  },
19
19
  {
20
- id: 'C456',
20
+ id: 'C0987654321',
21
21
  name: 'random',
22
22
  is_private: false,
23
23
  created: 1234567890,
24
24
  is_member: true,
25
25
  },
26
26
  {
27
- id: 'C789',
27
+ id: 'C1111111111',
28
28
  name: 'dev-team',
29
29
  is_private: false,
30
30
  created: 1234567890,
@@ -32,7 +32,7 @@ describe('ChannelResolver', () => {
32
32
  name_normalized: 'dev-team',
33
33
  },
34
34
  {
35
- id: 'G123',
35
+ id: 'G1234567890',
36
36
  name: 'private-channel',
37
37
  is_private: true,
38
38
  created: 1234567890,
@@ -42,21 +42,18 @@ describe('ChannelResolver', () => {
42
42
  });
43
43
 
44
44
  describe('isChannelId', () => {
45
- it('should identify channel IDs starting with C', () => {
46
- expect(resolver.isChannelId('C123456')).toBe(true);
45
+ it('should identify channel IDs with valid format', () => {
46
+ expect(resolver.isChannelId('C1234567890')).toBe(true);
47
+ expect(resolver.isChannelId('D1234567890')).toBe(true);
48
+ expect(resolver.isChannelId('G1234567890')).toBe(true);
47
49
  });
48
50
 
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', () => {
51
+ it('should return false for channel names or malformed IDs', () => {
58
52
  expect(resolver.isChannelId('general')).toBe(false);
59
53
  expect(resolver.isChannelId('#general')).toBe(false);
54
+ expect(resolver.isChannelId('General')).toBe(false);
55
+ expect(resolver.isChannelId('Dev')).toBe(false);
56
+ expect(resolver.isChannelId('C123')).toBe(false);
60
57
  });
61
58
  });
62
59
 
@@ -96,8 +93,8 @@ describe('ChannelResolver', () => {
96
93
  it('should limit results to specified count', () => {
97
94
  const manyChannels = [
98
95
  ...mockChannels,
99
- { id: 'C999', name: 'general-2', is_private: false, created: 0 },
100
- { id: 'C888', name: 'general-3', is_private: false, created: 0 },
96
+ { id: 'C9999999999', name: 'general-2', is_private: false, created: 0 },
97
+ { id: 'C8888888888', name: 'general-3', is_private: false, created: 0 },
101
98
  ];
102
99
  const result = resolver.getSimilarChannels('general', manyChannels, 2);
103
100
  expect(result).toHaveLength(2);
@@ -128,15 +125,22 @@ describe('ChannelResolver', () => {
128
125
  describe('resolveChannelId', () => {
129
126
  it('should return ID directly if already an ID', async () => {
130
127
  const getChannelsFn = vi.fn();
131
- const result = await resolver.resolveChannelId('C123456', getChannelsFn);
132
- expect(result).toBe('C123456');
128
+ const result = await resolver.resolveChannelId('C1234567890', getChannelsFn);
129
+ expect(result).toBe('C1234567890');
133
130
  expect(getChannelsFn).not.toHaveBeenCalled();
134
131
  });
135
132
 
136
133
  it('should resolve channel name to ID', async () => {
137
134
  const getChannelsFn = vi.fn().mockResolvedValue(mockChannels);
138
135
  const result = await resolver.resolveChannelId('general', getChannelsFn);
139
- expect(result).toBe('C123');
136
+ expect(result).toBe('C1234567890');
137
+ expect(getChannelsFn).toHaveBeenCalled();
138
+ });
139
+
140
+ it('should resolve mixed-case channel names to ID', async () => {
141
+ const getChannelsFn = vi.fn().mockResolvedValue(mockChannels);
142
+ const result = await resolver.resolveChannelId('General', getChannelsFn);
143
+ expect(result).toBe('C1234567890');
140
144
  expect(getChannelsFn).toHaveBeenCalled();
141
145
  });
142
146
 
@@ -154,4 +158,4 @@ describe('ChannelResolver', () => {
154
158
  );
155
159
  });
156
160
  });
157
- });
161
+ });
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { parseScheduledTimestamp, resolvePostAt } from '../../src/utils/schedule-utils';
3
+ import { optionValidators } from '../../src/utils/validators';
4
+
5
+ describe('schedule utils', () => {
6
+ describe('parseScheduledTimestamp', () => {
7
+ it('parses unix timestamp seconds', () => {
8
+ expect(parseScheduledTimestamp('1770855000')).toBe(1770855000);
9
+ });
10
+
11
+ it('parses ISO date string', () => {
12
+ expect(parseScheduledTimestamp('2026-02-12T00:10:00Z')).toBe(1770855000);
13
+ });
14
+
15
+ it('returns null for invalid input', () => {
16
+ expect(parseScheduledTimestamp('invalid')).toBeNull();
17
+ });
18
+ });
19
+
20
+ describe('resolvePostAt', () => {
21
+ it('returns parsed timestamp for --at', () => {
22
+ expect(resolvePostAt('1770855000', undefined)).toBe(1770855000);
23
+ });
24
+
25
+ it('returns now + minutes for --after', () => {
26
+ expect(resolvePostAt(undefined, '10', Date.parse('2026-02-12T00:00:00Z'))).toBe(1770855000);
27
+ });
28
+
29
+ it('returns null when invalid --after is provided', () => {
30
+ expect(resolvePostAt(undefined, '0')).toBeNull();
31
+ expect(resolvePostAt(undefined, '1.5')).toBeNull();
32
+ expect(resolvePostAt(undefined, '10minutes')).toBeNull();
33
+ });
34
+ });
35
+
36
+ describe('optionValidators.scheduleTiming', () => {
37
+ it('rejects both --at and --after', () => {
38
+ expect(optionValidators.scheduleTiming({ at: '1770855000', after: '10' })).toBe(
39
+ 'Cannot use both --at and --after'
40
+ );
41
+ });
42
+
43
+ it('rejects past --at timestamp', () => {
44
+ vi.useFakeTimers();
45
+ vi.setSystemTime(new Date('2026-02-12T00:00:00Z'));
46
+
47
+ expect(optionValidators.scheduleTiming({ at: '1770854300' })).toBe(
48
+ 'Schedule time must be in the future'
49
+ );
50
+
51
+ vi.useRealTimers();
52
+ });
53
+
54
+ it('rejects non-integer --after values', () => {
55
+ expect(optionValidators.scheduleTiming({ after: '1.5' })).toBe(
56
+ '--after must be a positive integer (minutes)'
57
+ );
58
+ expect(optionValidators.scheduleTiming({ after: '10minutes' })).toBe(
59
+ '--after must be a positive integer (minutes)'
60
+ );
61
+ });
62
+ });
63
+ });