@urugus/slack-cli 0.2.8 → 0.2.10

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 (53) hide show
  1. package/.claude/settings.local.json +6 -2
  2. package/.github/workflows/ci.yml +2 -2
  3. package/README.md +8 -0
  4. package/dist/commands/send.d.ts.map +1 -1
  5. package/dist/commands/send.js +16 -2
  6. package/dist/commands/send.js.map +1 -1
  7. package/dist/commands/unread.d.ts.map +1 -1
  8. package/dist/commands/unread.js +32 -16
  9. package/dist/commands/unread.js.map +1 -1
  10. package/dist/types/commands.d.ts +2 -0
  11. package/dist/types/commands.d.ts.map +1 -1
  12. package/dist/utils/constants.d.ts +5 -0
  13. package/dist/utils/constants.d.ts.map +1 -1
  14. package/dist/utils/constants.js +5 -0
  15. package/dist/utils/constants.js.map +1 -1
  16. package/dist/utils/errors.d.ts.map +1 -1
  17. package/dist/utils/errors.js +1 -5
  18. package/dist/utils/errors.js.map +1 -1
  19. package/dist/utils/schedule-utils.d.ts +3 -0
  20. package/dist/utils/schedule-utils.d.ts.map +1 -0
  21. package/dist/utils/schedule-utils.js +34 -0
  22. package/dist/utils/schedule-utils.js.map +1 -0
  23. package/dist/utils/slack-api-client.d.ts +2 -1
  24. package/dist/utils/slack-api-client.d.ts.map +1 -1
  25. package/dist/utils/slack-api-client.js +3 -0
  26. package/dist/utils/slack-api-client.js.map +1 -1
  27. package/dist/utils/slack-operations/message-operations.d.ts +2 -1
  28. package/dist/utils/slack-operations/message-operations.d.ts.map +1 -1
  29. package/dist/utils/slack-operations/message-operations.js +11 -0
  30. package/dist/utils/slack-operations/message-operations.js.map +1 -1
  31. package/dist/utils/validators.d.ts +4 -0
  32. package/dist/utils/validators.d.ts.map +1 -1
  33. package/dist/utils/validators.js +31 -0
  34. package/dist/utils/validators.js.map +1 -1
  35. package/eslint.config.js +38 -0
  36. package/package.json +13 -15
  37. package/src/commands/send.ts +21 -3
  38. package/src/commands/unread.ts +52 -22
  39. package/src/types/commands.ts +2 -0
  40. package/src/utils/constants.ts +7 -0
  41. package/src/utils/errors.ts +1 -5
  42. package/src/utils/schedule-utils.ts +41 -0
  43. package/src/utils/slack-api-client.ts +10 -1
  44. package/src/utils/slack-operations/message-operations.ts +25 -1
  45. package/src/utils/validators.ts +38 -0
  46. package/tests/commands/send.test.ts +235 -44
  47. package/tests/index.test.ts +2 -2
  48. package/tests/utils/schedule-utils.test.ts +63 -0
  49. package/tests/utils/slack-api-client.test.ts +18 -1
  50. package/tests/utils/slack-operations/message-operations.test.ts +19 -1
  51. package/.eslintrc.json +0 -25
  52. package/src/utils/formatters/output-formatter.ts +0 -7
  53. package/tests/utils/slack-operations/channel-operations-refactored.test.ts +0 -179
@@ -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
+ });
@@ -4,7 +4,7 @@ import { readFileSync } from 'fs';
4
4
  import { join } from 'path';
5
5
 
6
6
  describe('slack-cli version', () => {
7
- it('should display the correct version from package.json', () => {
7
+ it('should display the correct version from package.json', { timeout: 20000 }, () => {
8
8
  // Read the expected version from package.json
9
9
  const packageJson = JSON.parse(
10
10
  readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')
@@ -21,7 +21,7 @@ describe('slack-cli version', () => {
21
21
  expect(output).toBe(expectedVersion);
22
22
  });
23
23
 
24
- it('should display version with -V flag', () => {
24
+ it('should display version with -V flag', { timeout: 20000 }, () => {
25
25
  // Read the expected version from package.json
26
26
  const packageJson = JSON.parse(
27
27
  readFileSync(join(__dirname, '..', 'package.json'), 'utf-8')
@@ -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
+ });
@@ -12,7 +12,8 @@ describe('SlackApiClient', () => {
12
12
  vi.clearAllMocks();
13
13
  mockWebClient = {
14
14
  chat: {
15
- postMessage: vi.fn()
15
+ postMessage: vi.fn(),
16
+ scheduleMessage: vi.fn()
16
17
  },
17
18
  conversations: {
18
19
  list: vi.fn(),
@@ -84,6 +85,22 @@ describe('SlackApiClient', () => {
84
85
  });
85
86
  });
86
87
 
88
+ describe('scheduleMessage', () => {
89
+ it('should schedule message to channel', async () => {
90
+ const mockResponse = { ok: true, scheduled_message_id: 'Q123', post_at: 1770855000 };
91
+ vi.mocked(mockWebClient.chat.scheduleMessage).mockResolvedValue(mockResponse as any);
92
+
93
+ const result = await client.scheduleMessage('general', 'Hello, future!', 1770855000);
94
+
95
+ expect(mockWebClient.chat.scheduleMessage).toHaveBeenCalledWith({
96
+ channel: 'general',
97
+ text: 'Hello, future!',
98
+ post_at: 1770855000
99
+ });
100
+ expect(result).toEqual(mockResponse);
101
+ });
102
+ });
103
+
87
104
  describe('listChannels', () => {
88
105
  it('should list channels with default options', async () => {
89
106
  const mockChannels = [
@@ -12,6 +12,7 @@ vi.mock('@slack/web-api', () => ({
12
12
  },
13
13
  chat: {
14
14
  postMessage: vi.fn(),
15
+ scheduleMessage: vi.fn(),
15
16
  },
16
17
  })),
17
18
  LogLevel: {
@@ -31,6 +32,23 @@ describe('MessageOperations', () => {
31
32
  mockClient = (messageOps as any).client;
32
33
  });
33
34
 
35
+ describe('scheduleMessage', () => {
36
+ it('should call chat.scheduleMessage with post_at', async () => {
37
+ mockClient.chat.scheduleMessage.mockResolvedValue({
38
+ ok: true,
39
+ scheduled_message_id: 'Q123',
40
+ });
41
+
42
+ await messageOps.scheduleMessage('C1234567890', 'Hello', 1770855000);
43
+
44
+ expect(mockClient.chat.scheduleMessage).toHaveBeenCalledWith({
45
+ channel: 'C1234567890',
46
+ text: 'Hello',
47
+ post_at: 1770855000,
48
+ });
49
+ });
50
+ });
51
+
34
52
  describe('getHistory with mentions', () => {
35
53
  it('should fetch user info for mentioned users in message text', async () => {
36
54
  const mockMessages = [
@@ -123,4 +141,4 @@ describe('MessageOperations', () => {
123
141
  expect(result.users.get('U07L5D50RAL')).toBe('koguchi_s');
124
142
  });
125
143
  });
126
- });
144
+ });
package/.eslintrc.json DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "parser": "@typescript-eslint/parser",
3
- "extends": [
4
- "eslint:recommended",
5
- "plugin:@typescript-eslint/recommended",
6
- "prettier"
7
- ],
8
- "plugins": ["@typescript-eslint"],
9
- "parserOptions": {
10
- "ecmaVersion": 2020,
11
- "sourceType": "module",
12
- "project": "./tsconfig.json"
13
- },
14
- "env": {
15
- "node": true,
16
- "es2020": true
17
- },
18
- "rules": {
19
- "@typescript-eslint/explicit-function-return-type": "off",
20
- "@typescript-eslint/no-explicit-any": "warn",
21
- "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
22
- "no-console": "off"
23
- },
24
- "ignorePatterns": ["dist", "node_modules", "coverage", "vitest.config.ts"]
25
- }
@@ -1,7 +0,0 @@
1
- export interface OutputFormatter<T> {
2
- format(data: T[]): void;
3
- }
4
-
5
- export abstract class BaseFormatter<T> implements OutputFormatter<T> {
6
- abstract format(data: T[]): void;
7
- }