digital-tools 2.1.3 → 2.4.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.
Files changed (294) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +2 -0
  4. package/dist/client.d.ts +109 -0
  5. package/dist/client.d.ts.map +1 -0
  6. package/dist/client.js +69 -0
  7. package/dist/client.js.map +1 -0
  8. package/dist/define.d.ts +2 -2
  9. package/dist/define.d.ts.map +1 -1
  10. package/dist/define.js +21 -11
  11. package/dist/define.js.map +1 -1
  12. package/dist/function-ref.d.ts +229 -0
  13. package/dist/function-ref.d.ts.map +1 -0
  14. package/dist/function-ref.js +28 -0
  15. package/dist/function-ref.js.map +1 -0
  16. package/dist/function-sugar.d.ts +57 -0
  17. package/dist/function-sugar.d.ts.map +1 -0
  18. package/dist/function-sugar.js +79 -0
  19. package/dist/function-sugar.js.map +1 -0
  20. package/dist/index.d.ts +10 -3
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +24 -4
  23. package/dist/index.js.map +1 -1
  24. package/dist/providers/analytics/mixpanel.d.ts.map +1 -1
  25. package/dist/providers/analytics/mixpanel.js +21 -18
  26. package/dist/providers/analytics/mixpanel.js.map +1 -1
  27. package/dist/providers/calendar/cal-com.d.ts.map +1 -1
  28. package/dist/providers/calendar/cal-com.js +10 -10
  29. package/dist/providers/calendar/cal-com.js.map +1 -1
  30. package/dist/providers/calendar/google-calendar.d.ts.map +1 -1
  31. package/dist/providers/calendar/google-calendar.js +4 -4
  32. package/dist/providers/calendar/google-calendar.js.map +1 -1
  33. package/dist/providers/crm/hubspot.d.ts.map +1 -1
  34. package/dist/providers/crm/hubspot.js +107 -85
  35. package/dist/providers/crm/hubspot.js.map +1 -1
  36. package/dist/providers/development/github.d.ts.map +1 -1
  37. package/dist/providers/development/github.js +40 -43
  38. package/dist/providers/development/github.js.map +1 -1
  39. package/dist/providers/ecommerce/shopify.d.ts.map +1 -1
  40. package/dist/providers/ecommerce/shopify.js +79 -62
  41. package/dist/providers/ecommerce/shopify.js.map +1 -1
  42. package/dist/providers/email/resend.d.ts.map +1 -1
  43. package/dist/providers/email/resend.js +20 -16
  44. package/dist/providers/email/resend.js.map +1 -1
  45. package/dist/providers/email/sendgrid.d.ts.map +1 -1
  46. package/dist/providers/email/sendgrid.js +12 -9
  47. package/dist/providers/email/sendgrid.js.map +1 -1
  48. package/dist/providers/finance/stripe.d.ts.map +1 -1
  49. package/dist/providers/finance/stripe.js +44 -42
  50. package/dist/providers/finance/stripe.js.map +1 -1
  51. package/dist/providers/forms/typeform.d.ts.map +1 -1
  52. package/dist/providers/forms/typeform.js +68 -58
  53. package/dist/providers/forms/typeform.js.map +1 -1
  54. package/dist/providers/knowledge/notion.d.ts.map +1 -1
  55. package/dist/providers/knowledge/notion.js +75 -41
  56. package/dist/providers/knowledge/notion.js.map +1 -1
  57. package/dist/providers/marketing/mailchimp.d.ts.map +1 -1
  58. package/dist/providers/marketing/mailchimp.js +74 -61
  59. package/dist/providers/marketing/mailchimp.js.map +1 -1
  60. package/dist/providers/media/cloudinary.d.ts.map +1 -1
  61. package/dist/providers/media/cloudinary.js +30 -28
  62. package/dist/providers/media/cloudinary.js.map +1 -1
  63. package/dist/providers/messaging/slack.d.ts.map +1 -1
  64. package/dist/providers/messaging/slack.js +75 -58
  65. package/dist/providers/messaging/slack.js.map +1 -1
  66. package/dist/providers/messaging/twilio-sms.d.ts.map +1 -1
  67. package/dist/providers/messaging/twilio-sms.js +33 -15
  68. package/dist/providers/messaging/twilio-sms.js.map +1 -1
  69. package/dist/providers/project-management/linear.d.ts.map +1 -1
  70. package/dist/providers/project-management/linear.js +31 -27
  71. package/dist/providers/project-management/linear.js.map +1 -1
  72. package/dist/providers/spreadsheet/google-sheets.d.ts.map +1 -1
  73. package/dist/providers/spreadsheet/google-sheets.js +21 -18
  74. package/dist/providers/spreadsheet/google-sheets.js.map +1 -1
  75. package/dist/providers/spreadsheet/xlsx.d.ts.map +1 -1
  76. package/dist/providers/spreadsheet/xlsx.js +4 -4
  77. package/dist/providers/spreadsheet/xlsx.js.map +1 -1
  78. package/dist/providers/storage/index.js +1 -0
  79. package/dist/providers/storage/index.js.map +1 -1
  80. package/dist/providers/storage/s3.d.ts.map +1 -1
  81. package/dist/providers/storage/s3.js +36 -27
  82. package/dist/providers/storage/s3.js.map +1 -1
  83. package/dist/providers/support/zendesk.d.ts.map +1 -1
  84. package/dist/providers/support/zendesk.js +24 -25
  85. package/dist/providers/support/zendesk.js.map +1 -1
  86. package/dist/providers/tasks/todoist.d.ts.map +1 -1
  87. package/dist/providers/tasks/todoist.js +18 -18
  88. package/dist/providers/tasks/todoist.js.map +1 -1
  89. package/dist/providers/video-conferencing/google-meet.d.ts.map +1 -1
  90. package/dist/providers/video-conferencing/google-meet.js +11 -11
  91. package/dist/providers/video-conferencing/google-meet.js.map +1 -1
  92. package/dist/providers/video-conferencing/jitsi.js +14 -14
  93. package/dist/providers/video-conferencing/jitsi.js.map +1 -1
  94. package/dist/providers/video-conferencing/teams.d.ts.map +1 -1
  95. package/dist/providers/video-conferencing/teams.js +9 -7
  96. package/dist/providers/video-conferencing/teams.js.map +1 -1
  97. package/dist/providers/video-conferencing/zoom.d.ts.map +1 -1
  98. package/dist/providers/video-conferencing/zoom.js +26 -24
  99. package/dist/providers/video-conferencing/zoom.js.map +1 -1
  100. package/dist/tools/data.d.ts.map +1 -1
  101. package/dist/tools/data.js +5 -12
  102. package/dist/tools/data.js.map +1 -1
  103. package/dist/tools/index.d.ts +1 -0
  104. package/dist/tools/index.d.ts.map +1 -1
  105. package/dist/tools/index.js +1 -0
  106. package/dist/tools/index.js.map +1 -1
  107. package/dist/tools/system.d.ts +289 -0
  108. package/dist/tools/system.d.ts.map +1 -0
  109. package/dist/tools/system.js +752 -0
  110. package/dist/tools/system.js.map +1 -0
  111. package/dist/tools/web.d.ts.map +1 -1
  112. package/dist/tools/web.js +22 -10
  113. package/dist/tools/web.js.map +1 -1
  114. package/dist/track-record.d.ts +101 -0
  115. package/dist/track-record.d.ts.map +1 -0
  116. package/dist/track-record.js +17 -0
  117. package/dist/track-record.js.map +1 -0
  118. package/dist/types.d.ts +210 -9
  119. package/dist/types.d.ts.map +1 -1
  120. package/dist/verb-registration.d.ts +122 -0
  121. package/dist/verb-registration.d.ts.map +1 -0
  122. package/dist/verb-registration.js +176 -0
  123. package/dist/verb-registration.js.map +1 -0
  124. package/dist/worker.d.ts +93 -0
  125. package/dist/worker.d.ts.map +1 -0
  126. package/dist/worker.js +315 -0
  127. package/dist/worker.js.map +1 -0
  128. package/dist/wrap.d.ts +89 -0
  129. package/dist/wrap.d.ts.map +1 -0
  130. package/dist/wrap.js +225 -0
  131. package/dist/wrap.js.map +1 -0
  132. package/package.json +31 -14
  133. package/src/client.ts +136 -0
  134. package/src/define.ts +30 -24
  135. package/src/function-ref.ts +264 -0
  136. package/src/function-sugar.ts +134 -0
  137. package/src/index.ts +132 -10
  138. package/src/providers/analytics/mixpanel.ts +19 -18
  139. package/src/providers/calendar/cal-com.ts +29 -18
  140. package/src/providers/calendar/google-calendar.ts +20 -14
  141. package/src/providers/crm/hubspot.ts +225 -99
  142. package/src/providers/development/github.ts +206 -135
  143. package/src/providers/ecommerce/shopify.ts +250 -89
  144. package/src/providers/email/resend.ts +101 -28
  145. package/src/providers/email/sendgrid.ts +12 -9
  146. package/src/providers/finance/stripe.ts +128 -49
  147. package/src/providers/forms/typeform.ts +74 -58
  148. package/src/providers/knowledge/notion.ts +340 -88
  149. package/src/providers/marketing/mailchimp.ts +86 -70
  150. package/src/providers/media/cloudinary.ts +99 -41
  151. package/src/providers/messaging/slack.ts +283 -85
  152. package/src/providers/messaging/twilio-sms.ts +35 -15
  153. package/src/providers/project-management/linear.ts +143 -55
  154. package/src/providers/spreadsheet/google-sheets.ts +222 -56
  155. package/src/providers/spreadsheet/xlsx.ts +47 -16
  156. package/src/providers/storage/s3.ts +119 -47
  157. package/src/providers/support/zendesk.ts +196 -46
  158. package/src/providers/tasks/todoist.ts +20 -26
  159. package/src/providers/video-conferencing/google-meet.ts +17 -20
  160. package/src/providers/video-conferencing/jitsi.ts +14 -14
  161. package/src/providers/video-conferencing/teams.ts +14 -13
  162. package/src/providers/video-conferencing/zoom.ts +54 -49
  163. package/src/tools/data.ts +6 -16
  164. package/src/tools/index.ts +1 -0
  165. package/src/tools/system.ts +887 -0
  166. package/src/tools/web.ts +22 -10
  167. package/src/track-record.ts +106 -0
  168. package/src/types.ts +241 -13
  169. package/src/verb-registration.ts +197 -0
  170. package/src/worker.ts +370 -0
  171. package/src/wrap.ts +260 -0
  172. package/test/client.test.ts +146 -0
  173. package/test/communication-tools-extended.test.ts +734 -0
  174. package/test/data-tools-extended.test.ts +743 -0
  175. package/test/define-extended.test.ts +819 -0
  176. package/test/define.test.ts +150 -41
  177. package/test/entities.test.ts +623 -0
  178. package/test/extended-entities.test.ts +1228 -0
  179. package/test/provider-implementations.test.ts +725 -0
  180. package/test/provider-registry-extended.test.ts +583 -0
  181. package/test/providers/google-sheets.test.ts +851 -0
  182. package/test/providers/helpers.ts +554 -0
  183. package/test/providers/hubspot.test.ts +576 -0
  184. package/test/providers/slack.test.ts +932 -0
  185. package/test/providers/stripe.test.ts +701 -0
  186. package/test/providers.test.ts +578 -0
  187. package/test/system-tools-extended.test.ts +632 -0
  188. package/test/system.test.ts +673 -0
  189. package/test/tools.test.ts +15 -11
  190. package/test/types.test.ts +402 -0
  191. package/test/verb-registration.test.ts +395 -0
  192. package/test/web-tools.test.ts +553 -0
  193. package/test/worker-extended.test.ts +699 -0
  194. package/test/worker.test.ts +576 -0
  195. package/test/wrap.test.ts +366 -0
  196. package/tsconfig.json +3 -13
  197. package/vitest.config.ts +37 -0
  198. package/wrangler.jsonc +9 -0
  199. package/LICENSE +0 -21
  200. package/dist/providers/voice/vapi.d.ts +0 -27
  201. package/dist/providers/voice/vapi.d.ts.map +0 -1
  202. package/dist/providers/voice/vapi.js +0 -440
  203. package/dist/providers/voice/vapi.js.map +0 -1
  204. package/src/define.js +0 -259
  205. package/src/entities/advertising.js +0 -999
  206. package/src/entities/ai.js +0 -756
  207. package/src/entities/analytics.js +0 -1588
  208. package/src/entities/automation.js +0 -601
  209. package/src/entities/communication.js +0 -1150
  210. package/src/entities/crm.js +0 -1386
  211. package/src/entities/design.js +0 -546
  212. package/src/entities/development.js +0 -2212
  213. package/src/entities/document.js +0 -874
  214. package/src/entities/ecommerce.js +0 -1429
  215. package/src/entities/experiment.js +0 -1039
  216. package/src/entities/finance.js +0 -3478
  217. package/src/entities/forms.js +0 -1892
  218. package/src/entities/hr.js +0 -661
  219. package/src/entities/identity.js +0 -997
  220. package/src/entities/index.js +0 -282
  221. package/src/entities/infrastructure.js +0 -1153
  222. package/src/entities/knowledge.js +0 -1438
  223. package/src/entities/marketing.js +0 -1610
  224. package/src/entities/media.js +0 -1634
  225. package/src/entities/notification.js +0 -1199
  226. package/src/entities/presentation.js +0 -1274
  227. package/src/entities/productivity.js +0 -1317
  228. package/src/entities/project-management.js +0 -1136
  229. package/src/entities/recruiting.js +0 -736
  230. package/src/entities/shipping.js +0 -509
  231. package/src/entities/signature.js +0 -1102
  232. package/src/entities/site.js +0 -222
  233. package/src/entities/spreadsheet.js +0 -1341
  234. package/src/entities/storage.js +0 -1198
  235. package/src/entities/support.js +0 -1166
  236. package/src/entities/video-conferencing.js +0 -1750
  237. package/src/entities/video.js +0 -950
  238. package/src/entities.js +0 -1663
  239. package/src/index.js +0 -74
  240. package/src/providers/analytics/index.js +0 -17
  241. package/src/providers/analytics/mixpanel.js +0 -255
  242. package/src/providers/calendar/cal-com.js +0 -303
  243. package/src/providers/calendar/google-calendar.js +0 -335
  244. package/src/providers/calendar/index.js +0 -20
  245. package/src/providers/crm/hubspot.js +0 -566
  246. package/src/providers/crm/index.js +0 -17
  247. package/src/providers/development/github.js +0 -472
  248. package/src/providers/development/index.js +0 -17
  249. package/src/providers/ecommerce/index.js +0 -17
  250. package/src/providers/ecommerce/shopify.js +0 -378
  251. package/src/providers/email/index.js +0 -20
  252. package/src/providers/email/resend.js +0 -258
  253. package/src/providers/email/sendgrid.js +0 -161
  254. package/src/providers/finance/index.js +0 -17
  255. package/src/providers/finance/stripe.js +0 -549
  256. package/src/providers/forms/index.js +0 -17
  257. package/src/providers/forms/typeform.js +0 -500
  258. package/src/providers/index.js +0 -123
  259. package/src/providers/knowledge/index.js +0 -17
  260. package/src/providers/knowledge/notion.js +0 -389
  261. package/src/providers/marketing/index.js +0 -17
  262. package/src/providers/marketing/mailchimp.js +0 -443
  263. package/src/providers/media/cloudinary.js +0 -318
  264. package/src/providers/media/index.js +0 -17
  265. package/src/providers/messaging/index.js +0 -20
  266. package/src/providers/messaging/slack.js +0 -393
  267. package/src/providers/messaging/twilio-sms.js +0 -249
  268. package/src/providers/project-management/index.js +0 -17
  269. package/src/providers/project-management/linear.js +0 -575
  270. package/src/providers/registry.js +0 -86
  271. package/src/providers/spreadsheet/google-sheets.js +0 -375
  272. package/src/providers/spreadsheet/index.js +0 -20
  273. package/src/providers/spreadsheet/xlsx.js +0 -423
  274. package/src/providers/storage/index.js +0 -24
  275. package/src/providers/storage/s3.js +0 -419
  276. package/src/providers/support/index.js +0 -17
  277. package/src/providers/support/zendesk.js +0 -373
  278. package/src/providers/tasks/index.js +0 -17
  279. package/src/providers/tasks/todoist.js +0 -286
  280. package/src/providers/types.js +0 -9
  281. package/src/providers/video-conferencing/google-meet.js +0 -286
  282. package/src/providers/video-conferencing/index.js +0 -31
  283. package/src/providers/video-conferencing/jitsi.js +0 -254
  284. package/src/providers/video-conferencing/teams.js +0 -270
  285. package/src/providers/video-conferencing/zoom.js +0 -332
  286. package/src/registry.js +0 -128
  287. package/src/tools/communication.js +0 -184
  288. package/src/tools/data.js +0 -205
  289. package/src/tools/index.js +0 -11
  290. package/src/tools/web.js +0 -137
  291. package/src/types.js +0 -10
  292. package/test/define.test.js +0 -306
  293. package/test/registry.test.js +0 -357
  294. package/test/tools.test.js +0 -363
@@ -0,0 +1,932 @@
1
+ /**
2
+ * Slack Messaging Provider Tests
3
+ *
4
+ * Tests for the Slack messaging provider implementation covering:
5
+ * - Provider initialization with access/bot tokens
6
+ * - Message sending, editing, and deletion
7
+ * - Channel operations
8
+ * - Member and workspace management
9
+ * - Reactions and presence
10
+ * - Error handling
11
+ */
12
+
13
+ import { describe, it, expect, beforeEach, afterEach, vi, type MockInstance } from 'vitest'
14
+ import { createSlackProvider, slackInfo } from '../../src/providers/messaging/slack.js'
15
+ import type { MessagingProvider } from '../../src/providers/types.js'
16
+ import {
17
+ setupMockFetch,
18
+ resetMockFetch,
19
+ mockJsonResponse,
20
+ mockNetworkError,
21
+ getLastFetchCall,
22
+ getFetchCall,
23
+ parseFetchJsonBody,
24
+ slackMocks,
25
+ } from './helpers.js'
26
+
27
+ describe('Slack Messaging Provider', () => {
28
+ let mockFetch: MockInstance
29
+ let provider: MessagingProvider
30
+
31
+ beforeEach(() => {
32
+ mockFetch = setupMockFetch()
33
+ })
34
+
35
+ afterEach(() => {
36
+ resetMockFetch(mockFetch)
37
+ })
38
+
39
+ // ===========================================================================
40
+ // Provider Initialization Tests
41
+ // ===========================================================================
42
+
43
+ describe('initialization', () => {
44
+ it('should have correct provider info', () => {
45
+ const provider = createSlackProvider({})
46
+ expect(provider.info).toBe(slackInfo)
47
+ expect(provider.info.id).toBe('messaging.slack')
48
+ expect(provider.info.name).toBe('Slack')
49
+ expect(provider.info.category).toBe('messaging')
50
+ })
51
+
52
+ it('should require access token for initialization', async () => {
53
+ provider = createSlackProvider({})
54
+ await expect(provider.initialize({})).rejects.toThrow('token is required')
55
+ })
56
+
57
+ it('should initialize successfully with access token', async () => {
58
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
59
+ await expect(provider.initialize({ accessToken: 'xoxb-test-token' })).resolves.toBeUndefined()
60
+ })
61
+
62
+ it('should initialize successfully with bot token', async () => {
63
+ provider = createSlackProvider({ botToken: 'xoxb-bot-token' })
64
+ await expect(provider.initialize({ botToken: 'xoxb-bot-token' })).resolves.toBeUndefined()
65
+ })
66
+
67
+ it('should include requiredConfig in provider info', () => {
68
+ provider = createSlackProvider({})
69
+ expect(provider.info.requiredConfig).toContain('accessToken')
70
+ })
71
+
72
+ it('should include optionalConfig in provider info', () => {
73
+ provider = createSlackProvider({})
74
+ expect(provider.info.optionalConfig).toContain('botToken')
75
+ expect(provider.info.optionalConfig).toContain('signingSecret')
76
+ })
77
+ })
78
+
79
+ // ===========================================================================
80
+ // Health Check Tests
81
+ // ===========================================================================
82
+
83
+ describe('healthCheck', () => {
84
+ beforeEach(async () => {
85
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
86
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
87
+ })
88
+
89
+ it('should return healthy status on successful auth.test', async () => {
90
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse({ user: 'testbot' })))
91
+
92
+ const health = await provider.healthCheck()
93
+
94
+ expect(health.healthy).toBe(true)
95
+ expect(health.message).toBe('Connected as testbot')
96
+ expect(health.latencyMs).toBeGreaterThanOrEqual(0)
97
+ expect(health.checkedAt).toBeInstanceOf(Date)
98
+ })
99
+
100
+ it('should call auth.test endpoint for health check', async () => {
101
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse({ user: 'bot' })))
102
+
103
+ await provider.healthCheck()
104
+
105
+ const { url } = getLastFetchCall(mockFetch)
106
+ expect(url).toContain('auth.test')
107
+ })
108
+
109
+ it('should return unhealthy status on API error', async () => {
110
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.errorResponse('invalid_auth')))
111
+
112
+ const health = await provider.healthCheck()
113
+
114
+ expect(health.healthy).toBe(false)
115
+ expect(health.message).toBe('invalid_auth')
116
+ })
117
+
118
+ it('should return unhealthy status on network error', async () => {
119
+ mockFetch.mockRejectedValueOnce(mockNetworkError('Connection timeout'))
120
+
121
+ const health = await provider.healthCheck()
122
+
123
+ expect(health.healthy).toBe(false)
124
+ expect(health.message).toBe('Connection timeout')
125
+ })
126
+ })
127
+
128
+ // ===========================================================================
129
+ // Message Sending Tests
130
+ // ===========================================================================
131
+
132
+ describe('send', () => {
133
+ beforeEach(async () => {
134
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
135
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
136
+ })
137
+
138
+ it('should send message to channel', async () => {
139
+ mockFetch.mockResolvedValueOnce(
140
+ mockJsonResponse(slackMocks.postMessageResponse('1234567890.123456', 'C123'))
141
+ )
142
+
143
+ const result = await provider.send({
144
+ channel: 'C123',
145
+ text: 'Hello Slack!',
146
+ })
147
+
148
+ expect(result.success).toBe(true)
149
+ expect(result.messageId).toBe('1234567890.123456')
150
+ expect(result.channel).toBe('C123')
151
+ })
152
+
153
+ it('should format request body correctly', async () => {
154
+ mockFetch.mockResolvedValueOnce(
155
+ mockJsonResponse(slackMocks.postMessageResponse('ts', 'C123'))
156
+ )
157
+
158
+ await provider.send({
159
+ channel: 'C123',
160
+ text: 'Test message',
161
+ })
162
+
163
+ const body = parseFetchJsonBody(mockFetch) as { text: string; channel: string }
164
+ expect(body.text).toBe('Test message')
165
+ expect(body.channel).toBe('C123')
166
+ })
167
+
168
+ it('should send DM by opening conversation first', async () => {
169
+ mockFetch
170
+ .mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse({ channel: { id: 'D123' } })))
171
+ .mockResolvedValueOnce(mockJsonResponse(slackMocks.postMessageResponse('ts', 'D123')))
172
+
173
+ const result = await provider.send({
174
+ userId: 'U456',
175
+ text: 'Direct message',
176
+ })
177
+
178
+ expect(result.success).toBe(true)
179
+ expect(result.channel).toBe('D123')
180
+ expect(mockFetch).toHaveBeenCalledTimes(2)
181
+ })
182
+
183
+ it('should return error when DM open fails', async () => {
184
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.errorResponse('user_not_found')))
185
+
186
+ const result = await provider.send({
187
+ userId: 'U999',
188
+ text: 'Message',
189
+ })
190
+
191
+ expect(result.success).toBe(false)
192
+ expect(result.error?.code).toBe('user_not_found')
193
+ })
194
+
195
+ it('should return error when neither channel nor userId provided', async () => {
196
+ const result = await provider.send({
197
+ text: 'No target',
198
+ })
199
+
200
+ expect(result.success).toBe(false)
201
+ expect(result.error?.code).toBe('MISSING_TARGET')
202
+ })
203
+
204
+ it('should include thread_ts for threaded replies', async () => {
205
+ mockFetch.mockResolvedValueOnce(
206
+ mockJsonResponse(slackMocks.postMessageResponse('ts', 'C123'))
207
+ )
208
+
209
+ await provider.send({
210
+ channel: 'C123',
211
+ text: 'Reply',
212
+ threadId: '1234567890.000000',
213
+ })
214
+
215
+ const body = parseFetchJsonBody(mockFetch) as { thread_ts: string }
216
+ expect(body.thread_ts).toBe('1234567890.000000')
217
+ })
218
+
219
+ it('should include blocks when provided', async () => {
220
+ mockFetch.mockResolvedValueOnce(
221
+ mockJsonResponse(slackMocks.postMessageResponse('ts', 'C123'))
222
+ )
223
+
224
+ const blocks = [{ type: 'section', text: { type: 'mrkdwn', text: '*Bold*' } }]
225
+ await provider.send({
226
+ channel: 'C123',
227
+ text: 'Fallback',
228
+ blocks,
229
+ })
230
+
231
+ const body = parseFetchJsonBody(mockFetch) as { blocks: unknown[] }
232
+ expect(body.blocks).toEqual(blocks)
233
+ })
234
+
235
+ it('should include metadata when provided', async () => {
236
+ mockFetch.mockResolvedValueOnce(
237
+ mockJsonResponse(slackMocks.postMessageResponse('ts', 'C123'))
238
+ )
239
+
240
+ await provider.send({
241
+ channel: 'C123',
242
+ text: 'Message',
243
+ metadata: { key: 'value' },
244
+ })
245
+
246
+ const body = parseFetchJsonBody(mockFetch) as { metadata: unknown }
247
+ expect(body.metadata).toBeDefined()
248
+ })
249
+
250
+ it('should handle API error response', async () => {
251
+ mockFetch.mockResolvedValueOnce(
252
+ mockJsonResponse(slackMocks.errorResponse('channel_not_found'))
253
+ )
254
+
255
+ const result = await provider.send({
256
+ channel: 'C999',
257
+ text: 'Message',
258
+ })
259
+
260
+ expect(result.success).toBe(false)
261
+ expect(result.error?.code).toBe('channel_not_found')
262
+ })
263
+ })
264
+
265
+ // ===========================================================================
266
+ // Message Editing Tests
267
+ // ===========================================================================
268
+
269
+ describe('edit', () => {
270
+ beforeEach(async () => {
271
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
272
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
273
+ })
274
+
275
+ it('should edit existing message', async () => {
276
+ mockFetch.mockResolvedValueOnce(
277
+ mockJsonResponse(slackMocks.postMessageResponse('1234567890.123456', 'C123'))
278
+ )
279
+
280
+ const result = await provider.edit!('1234567890.123456', 'Updated text')
281
+
282
+ expect(result.success).toBe(true)
283
+ })
284
+
285
+ it('should call chat.update endpoint', async () => {
286
+ mockFetch.mockResolvedValueOnce(
287
+ mockJsonResponse(slackMocks.postMessageResponse('ts', 'C123'))
288
+ )
289
+
290
+ await provider.edit!('ts', 'New text')
291
+
292
+ const { url } = getLastFetchCall(mockFetch)
293
+ expect(url).toContain('chat.update')
294
+ })
295
+
296
+ it('should include blocks when editing', async () => {
297
+ mockFetch.mockResolvedValueOnce(
298
+ mockJsonResponse(slackMocks.postMessageResponse('ts', 'C123'))
299
+ )
300
+
301
+ const blocks = [{ type: 'section', text: { type: 'plain_text', text: 'Updated' } }]
302
+ await provider.edit!('ts', 'Text', blocks)
303
+
304
+ const body = parseFetchJsonBody(mockFetch) as { blocks: unknown[] }
305
+ expect(body.blocks).toEqual(blocks)
306
+ })
307
+ })
308
+
309
+ // ===========================================================================
310
+ // Message Deletion Tests
311
+ // ===========================================================================
312
+
313
+ describe('delete', () => {
314
+ beforeEach(async () => {
315
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
316
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
317
+ })
318
+
319
+ it('should delete message', async () => {
320
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
321
+
322
+ const result = await provider.delete!('1234567890.123456', 'C123')
323
+
324
+ expect(result).toBe(true)
325
+ })
326
+
327
+ it('should call chat.delete endpoint', async () => {
328
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
329
+
330
+ await provider.delete!('ts', 'C123')
331
+
332
+ const { url } = getLastFetchCall(mockFetch)
333
+ expect(url).toContain('chat.delete')
334
+ })
335
+
336
+ it('should return false on deletion failure', async () => {
337
+ mockFetch.mockResolvedValueOnce(
338
+ mockJsonResponse(slackMocks.errorResponse('message_not_found'))
339
+ )
340
+
341
+ const result = await provider.delete!('ts', 'C123')
342
+
343
+ expect(result).toBe(false)
344
+ })
345
+ })
346
+
347
+ // ===========================================================================
348
+ // Reaction Tests
349
+ // ===========================================================================
350
+
351
+ describe('react', () => {
352
+ beforeEach(async () => {
353
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
354
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
355
+ })
356
+
357
+ it('should add reaction to message', async () => {
358
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
359
+
360
+ const result = await provider.react!('ts', 'C123', 'thumbsup')
361
+
362
+ expect(result).toBe(true)
363
+ })
364
+
365
+ it('should strip colons from emoji name', async () => {
366
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
367
+
368
+ await provider.react!('ts', 'C123', ':thumbsup:')
369
+
370
+ const body = parseFetchJsonBody(mockFetch) as { name: string }
371
+ expect(body.name).toBe('thumbsup')
372
+ })
373
+
374
+ it('should call reactions.add endpoint', async () => {
375
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
376
+
377
+ await provider.react!('ts', 'C123', 'emoji')
378
+
379
+ const { url } = getLastFetchCall(mockFetch)
380
+ expect(url).toContain('reactions.add')
381
+ })
382
+ })
383
+
384
+ describe('unreact', () => {
385
+ beforeEach(async () => {
386
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
387
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
388
+ })
389
+
390
+ it('should remove reaction from message', async () => {
391
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
392
+
393
+ const result = await provider.unreact!('ts', 'C123', 'thumbsup')
394
+
395
+ expect(result).toBe(true)
396
+ })
397
+
398
+ it('should call reactions.remove endpoint', async () => {
399
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
400
+
401
+ await provider.unreact!('ts', 'C123', 'emoji')
402
+
403
+ const { url } = getLastFetchCall(mockFetch)
404
+ expect(url).toContain('reactions.remove')
405
+ })
406
+ })
407
+
408
+ // ===========================================================================
409
+ // Message Retrieval Tests
410
+ // ===========================================================================
411
+
412
+ describe('getMessage', () => {
413
+ beforeEach(async () => {
414
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
415
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
416
+ })
417
+
418
+ it('should retrieve message by timestamp', async () => {
419
+ mockFetch.mockResolvedValueOnce(
420
+ mockJsonResponse(
421
+ slackMocks.conversationsHistoryResponse([slackMocks.message('ts', 'C123', 'Hello')])
422
+ )
423
+ )
424
+
425
+ const message = await provider.getMessage!('ts', 'C123')
426
+
427
+ expect(message).not.toBeNull()
428
+ expect(message?.id).toBe('ts')
429
+ expect(message?.text).toBe('Hello')
430
+ })
431
+
432
+ it('should return null when message not found', async () => {
433
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.conversationsHistoryResponse([])))
434
+
435
+ const message = await provider.getMessage!('ts', 'C123')
436
+
437
+ expect(message).toBeNull()
438
+ })
439
+ })
440
+
441
+ describe('listMessages', () => {
442
+ beforeEach(async () => {
443
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
444
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
445
+ })
446
+
447
+ it('should return paginated messages', async () => {
448
+ mockFetch.mockResolvedValueOnce(
449
+ mockJsonResponse(
450
+ slackMocks.conversationsHistoryResponse(
451
+ [
452
+ slackMocks.message('ts1', 'C123', 'Message 1'),
453
+ slackMocks.message('ts2', 'C123', 'Message 2'),
454
+ ],
455
+ true,
456
+ 'cursor123'
457
+ )
458
+ )
459
+ )
460
+
461
+ const result = await provider.listMessages!('C123')
462
+
463
+ expect(result.items).toHaveLength(2)
464
+ expect(result.hasMore).toBe(true)
465
+ expect(result.nextCursor).toBe('cursor123')
466
+ })
467
+
468
+ it('should apply pagination options', async () => {
469
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.conversationsHistoryResponse([])))
470
+
471
+ await provider.listMessages!('C123', { limit: 50, cursor: 'cursor' })
472
+
473
+ const body = parseFetchJsonBody(mockFetch) as { limit: number; cursor: string }
474
+ expect(body.limit).toBe(50)
475
+ expect(body.cursor).toBe('cursor')
476
+ })
477
+
478
+ it('should apply date filters', async () => {
479
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.conversationsHistoryResponse([])))
480
+
481
+ const since = new Date('2024-01-01')
482
+ const until = new Date('2024-01-31')
483
+ await provider.listMessages!('C123', { since, until })
484
+
485
+ const body = parseFetchJsonBody(mockFetch) as { oldest: string; latest: string }
486
+ expect(body.oldest).toBeDefined()
487
+ expect(body.latest).toBeDefined()
488
+ })
489
+ })
490
+
491
+ describe('searchMessages', () => {
492
+ beforeEach(async () => {
493
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
494
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
495
+ })
496
+
497
+ it('should search messages by query', async () => {
498
+ mockFetch.mockResolvedValueOnce(
499
+ mockJsonResponse({
500
+ ok: true,
501
+ messages: {
502
+ matches: [
503
+ slackMocks.message('ts', 'C123', 'Found message', { channel: { id: 'C123' } }),
504
+ ],
505
+ paging: { pages: 1, page: 1 },
506
+ total: 1,
507
+ },
508
+ })
509
+ )
510
+
511
+ const result = await provider.searchMessages!('query')
512
+
513
+ expect(result.items).toHaveLength(1)
514
+ expect(result.total).toBe(1)
515
+ })
516
+ })
517
+
518
+ // ===========================================================================
519
+ // Channel Operations Tests
520
+ // ===========================================================================
521
+
522
+ describe('listChannels', () => {
523
+ beforeEach(async () => {
524
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
525
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
526
+ })
527
+
528
+ it('should return list of channels', async () => {
529
+ mockFetch.mockResolvedValueOnce(
530
+ mockJsonResponse(
531
+ slackMocks.conversationsListResponse([
532
+ slackMocks.channel('C1', 'general'),
533
+ slackMocks.channel('C2', 'random'),
534
+ ])
535
+ )
536
+ )
537
+
538
+ const result = await provider.listChannels!()
539
+
540
+ expect(result.items).toHaveLength(2)
541
+ expect(result.items[0].name).toBe('general')
542
+ })
543
+
544
+ it('should filter by channel types', async () => {
545
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.conversationsListResponse([])))
546
+
547
+ await provider.listChannels!({ types: ['private'] })
548
+
549
+ const body = parseFetchJsonBody(mockFetch) as { types: string }
550
+ expect(body.types).toContain('private_channel')
551
+ })
552
+
553
+ it('should exclude archived channels by default', async () => {
554
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.conversationsListResponse([])))
555
+
556
+ await provider.listChannels!()
557
+
558
+ const body = parseFetchJsonBody(mockFetch) as { exclude_archived: boolean }
559
+ expect(body.exclude_archived).toBe(true)
560
+ })
561
+ })
562
+
563
+ describe('getChannel', () => {
564
+ beforeEach(async () => {
565
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
566
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
567
+ })
568
+
569
+ it('should retrieve channel by ID', async () => {
570
+ mockFetch.mockResolvedValueOnce(
571
+ mockJsonResponse({
572
+ ok: true,
573
+ channel: slackMocks.channel('C123', 'general'),
574
+ })
575
+ )
576
+
577
+ const channel = await provider.getChannel!('C123')
578
+
579
+ expect(channel).not.toBeNull()
580
+ expect(channel?.id).toBe('C123')
581
+ expect(channel?.name).toBe('general')
582
+ })
583
+
584
+ it('should return null for non-existent channel', async () => {
585
+ mockFetch.mockResolvedValueOnce(
586
+ mockJsonResponse(slackMocks.errorResponse('channel_not_found'))
587
+ )
588
+
589
+ const channel = await provider.getChannel!('C999')
590
+
591
+ expect(channel).toBeNull()
592
+ })
593
+ })
594
+
595
+ describe('createChannel', () => {
596
+ beforeEach(async () => {
597
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
598
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
599
+ })
600
+
601
+ it('should create new channel', async () => {
602
+ mockFetch.mockResolvedValueOnce(
603
+ mockJsonResponse({
604
+ ok: true,
605
+ channel: slackMocks.channel('C456', 'new-channel'),
606
+ })
607
+ )
608
+
609
+ const channel = await provider.createChannel!('new-channel')
610
+
611
+ expect(channel.id).toBe('C456')
612
+ expect(channel.name).toBe('new-channel')
613
+ })
614
+
615
+ it('should create private channel when specified', async () => {
616
+ mockFetch.mockResolvedValueOnce(
617
+ mockJsonResponse({
618
+ ok: true,
619
+ channel: slackMocks.channel('C456', 'private', { is_private: true }),
620
+ })
621
+ )
622
+
623
+ await provider.createChannel!('private', { isPrivate: true })
624
+
625
+ const body = parseFetchJsonBody(mockFetch) as { is_private: boolean }
626
+ expect(body.is_private).toBe(true)
627
+ })
628
+
629
+ it('should set topic when provided', async () => {
630
+ mockFetch
631
+ .mockResolvedValueOnce(
632
+ mockJsonResponse({
633
+ ok: true,
634
+ channel: slackMocks.channel('C456', 'channel'),
635
+ })
636
+ )
637
+ .mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
638
+
639
+ await provider.createChannel!('channel', { topic: 'Channel topic' })
640
+
641
+ expect(mockFetch).toHaveBeenCalledTimes(2)
642
+ const { url } = getFetchCall(mockFetch, 1)
643
+ expect(url).toContain('conversations.setTopic')
644
+ })
645
+
646
+ it('should throw on creation failure', async () => {
647
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.errorResponse('name_taken')))
648
+
649
+ await expect(provider.createChannel!('existing')).rejects.toThrow('name_taken')
650
+ })
651
+ })
652
+
653
+ describe('archiveChannel', () => {
654
+ beforeEach(async () => {
655
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
656
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
657
+ })
658
+
659
+ it('should archive channel', async () => {
660
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
661
+
662
+ const result = await provider.archiveChannel!('C123')
663
+
664
+ expect(result).toBe(true)
665
+ })
666
+ })
667
+
668
+ describe('joinChannel', () => {
669
+ beforeEach(async () => {
670
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
671
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
672
+ })
673
+
674
+ it('should join channel', async () => {
675
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
676
+
677
+ const result = await provider.joinChannel!('C123')
678
+
679
+ expect(result).toBe(true)
680
+ })
681
+ })
682
+
683
+ describe('leaveChannel', () => {
684
+ beforeEach(async () => {
685
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
686
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
687
+ })
688
+
689
+ it('should leave channel', async () => {
690
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse()))
691
+
692
+ const result = await provider.leaveChannel!('C123')
693
+
694
+ expect(result).toBe(true)
695
+ })
696
+ })
697
+
698
+ // ===========================================================================
699
+ // Member Operations Tests
700
+ // ===========================================================================
701
+
702
+ describe('listMembers', () => {
703
+ beforeEach(async () => {
704
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
705
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
706
+ })
707
+
708
+ it('should list workspace members', async () => {
709
+ mockFetch.mockResolvedValueOnce(
710
+ mockJsonResponse(
711
+ slackMocks.usersListResponse([
712
+ slackMocks.user('U1', 'user1'),
713
+ slackMocks.user('U2', 'user2'),
714
+ ])
715
+ )
716
+ )
717
+
718
+ const result = await provider.listMembers!()
719
+
720
+ expect(result.items).toHaveLength(2)
721
+ expect(result.items[0].username).toBe('user1')
722
+ })
723
+
724
+ it('should list channel members when channel specified', async () => {
725
+ mockFetch
726
+ .mockResolvedValueOnce(
727
+ mockJsonResponse({
728
+ ok: true,
729
+ members: ['U1', 'U2'],
730
+ response_metadata: {},
731
+ })
732
+ )
733
+ .mockResolvedValueOnce(
734
+ mockJsonResponse({
735
+ ok: true,
736
+ user: slackMocks.user('U1', 'user1'),
737
+ })
738
+ )
739
+ .mockResolvedValueOnce(
740
+ mockJsonResponse({
741
+ ok: true,
742
+ user: slackMocks.user('U2', 'user2'),
743
+ })
744
+ )
745
+
746
+ const result = await provider.listMembers!({ channel: 'C123' })
747
+
748
+ expect(result.items).toHaveLength(2)
749
+ })
750
+
751
+ it('should filter out deleted users', async () => {
752
+ mockFetch.mockResolvedValueOnce(
753
+ mockJsonResponse(
754
+ slackMocks.usersListResponse([
755
+ slackMocks.user('U1', 'active'),
756
+ slackMocks.user('U2', 'deleted', { deleted: true }),
757
+ ])
758
+ )
759
+ )
760
+
761
+ const result = await provider.listMembers!()
762
+
763
+ expect(result.items).toHaveLength(1)
764
+ expect(result.items[0].username).toBe('active')
765
+ })
766
+ })
767
+
768
+ describe('getMember', () => {
769
+ beforeEach(async () => {
770
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
771
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
772
+ })
773
+
774
+ it('should retrieve member by ID', async () => {
775
+ mockFetch.mockResolvedValueOnce(
776
+ mockJsonResponse({
777
+ ok: true,
778
+ user: slackMocks.user('U123', 'testuser'),
779
+ })
780
+ )
781
+
782
+ const member = await provider.getMember!('U123')
783
+
784
+ expect(member).not.toBeNull()
785
+ expect(member?.id).toBe('U123')
786
+ expect(member?.username).toBe('testuser')
787
+ })
788
+
789
+ it('should return null for non-existent user', async () => {
790
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.errorResponse('user_not_found')))
791
+
792
+ const member = await provider.getMember!('U999')
793
+
794
+ expect(member).toBeNull()
795
+ })
796
+
797
+ it('should map user fields correctly', async () => {
798
+ mockFetch.mockResolvedValueOnce(
799
+ mockJsonResponse({
800
+ ok: true,
801
+ user: slackMocks.user('U123', 'dev', {
802
+ is_admin: true,
803
+ is_bot: false,
804
+ tz: 'America/Los_Angeles',
805
+ }),
806
+ })
807
+ )
808
+
809
+ const member = await provider.getMember!('U123')
810
+
811
+ expect(member?.isAdmin).toBe(true)
812
+ expect(member?.isBot).toBe(false)
813
+ expect(member?.timezone).toBe('America/Los_Angeles')
814
+ })
815
+ })
816
+
817
+ describe('getPresence', () => {
818
+ beforeEach(async () => {
819
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
820
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
821
+ })
822
+
823
+ it('should return online presence', async () => {
824
+ mockFetch.mockResolvedValueOnce(mockJsonResponse({ ok: true, presence: 'active' }))
825
+
826
+ const presence = await provider.getPresence!('U123')
827
+
828
+ expect(presence.userId).toBe('U123')
829
+ expect(presence.presence).toBe('online')
830
+ })
831
+
832
+ it('should return away presence', async () => {
833
+ mockFetch.mockResolvedValueOnce(mockJsonResponse({ ok: true, presence: 'away' }))
834
+
835
+ const presence = await provider.getPresence!('U123')
836
+
837
+ expect(presence.presence).toBe('away')
838
+ })
839
+ })
840
+
841
+ // ===========================================================================
842
+ // Workspace Tests
843
+ // ===========================================================================
844
+
845
+ describe('getWorkspace', () => {
846
+ beforeEach(async () => {
847
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
848
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
849
+ })
850
+
851
+ it('should retrieve workspace info', async () => {
852
+ mockFetch.mockResolvedValueOnce(
853
+ mockJsonResponse({
854
+ ok: true,
855
+ team: slackMocks.team('T123', 'Test Workspace', 'testworkspace'),
856
+ })
857
+ )
858
+
859
+ const workspace = await provider.getWorkspace!()
860
+
861
+ expect(workspace.id).toBe('T123')
862
+ expect(workspace.name).toBe('Test Workspace')
863
+ expect(workspace.domain).toBe('testworkspace')
864
+ })
865
+
866
+ it('should throw on API failure', async () => {
867
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.errorResponse('team_not_found')))
868
+
869
+ await expect(provider.getWorkspace!()).rejects.toThrow('team_not_found')
870
+ })
871
+ })
872
+
873
+ // ===========================================================================
874
+ // API Request Formatting Tests
875
+ // ===========================================================================
876
+
877
+ describe('API request formatting', () => {
878
+ beforeEach(async () => {
879
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
880
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
881
+ })
882
+
883
+ it('should use POST method for all API calls', async () => {
884
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse({ user: 'bot' })))
885
+
886
+ await provider.healthCheck()
887
+
888
+ const { options } = getLastFetchCall(mockFetch)
889
+ expect(options?.method).toBe('POST')
890
+ })
891
+
892
+ it('should use JSON content type', async () => {
893
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.postMessageResponse('ts', 'C')))
894
+
895
+ await provider.send({ channel: 'C', text: 'Test' })
896
+
897
+ const { options } = getLastFetchCall(mockFetch)
898
+ expect(options?.headers).toHaveProperty('Content-Type', 'application/json; charset=utf-8')
899
+ })
900
+
901
+ it('should include Bearer token authorization', async () => {
902
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.postMessageResponse('ts', 'C')))
903
+
904
+ await provider.send({ channel: 'C', text: 'Test' })
905
+
906
+ const { options } = getLastFetchCall(mockFetch)
907
+ expect(options?.headers).toHaveProperty('Authorization', 'Bearer xoxb-test-token')
908
+ })
909
+
910
+ it('should use correct base URL', async () => {
911
+ mockFetch.mockResolvedValueOnce(mockJsonResponse(slackMocks.okResponse({ user: 'bot' })))
912
+
913
+ await provider.healthCheck()
914
+
915
+ const { url } = getLastFetchCall(mockFetch)
916
+ expect(url).toContain('https://slack.com/api/')
917
+ })
918
+ })
919
+
920
+ // ===========================================================================
921
+ // Dispose Tests
922
+ // ===========================================================================
923
+
924
+ describe('dispose', () => {
925
+ it('should dispose without error', async () => {
926
+ provider = createSlackProvider({ accessToken: 'xoxb-test-token' })
927
+ await provider.initialize({ accessToken: 'xoxb-test-token' })
928
+
929
+ await expect(provider.dispose()).resolves.toBeUndefined()
930
+ })
931
+ })
932
+ })