digital-tools 2.1.1 → 2.3.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 (293) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +2 -0
  3. package/dist/client.d.ts +109 -0
  4. package/dist/client.d.ts.map +1 -0
  5. package/dist/client.js +69 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/define.d.ts +2 -2
  8. package/dist/define.d.ts.map +1 -1
  9. package/dist/define.js +22 -20
  10. package/dist/define.js.map +1 -1
  11. package/dist/function-ref.d.ts +229 -0
  12. package/dist/function-ref.d.ts.map +1 -0
  13. package/dist/function-ref.js +28 -0
  14. package/dist/function-ref.js.map +1 -0
  15. package/dist/function-sugar.d.ts +57 -0
  16. package/dist/function-sugar.d.ts.map +1 -0
  17. package/dist/function-sugar.js +79 -0
  18. package/dist/function-sugar.js.map +1 -0
  19. package/dist/index.d.ts +10 -3
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +24 -4
  22. package/dist/index.js.map +1 -1
  23. package/dist/providers/analytics/mixpanel.d.ts.map +1 -1
  24. package/dist/providers/analytics/mixpanel.js +21 -18
  25. package/dist/providers/analytics/mixpanel.js.map +1 -1
  26. package/dist/providers/calendar/cal-com.d.ts.map +1 -1
  27. package/dist/providers/calendar/cal-com.js +10 -10
  28. package/dist/providers/calendar/cal-com.js.map +1 -1
  29. package/dist/providers/calendar/google-calendar.d.ts.map +1 -1
  30. package/dist/providers/calendar/google-calendar.js +4 -4
  31. package/dist/providers/calendar/google-calendar.js.map +1 -1
  32. package/dist/providers/crm/hubspot.d.ts.map +1 -1
  33. package/dist/providers/crm/hubspot.js +107 -85
  34. package/dist/providers/crm/hubspot.js.map +1 -1
  35. package/dist/providers/development/github.d.ts.map +1 -1
  36. package/dist/providers/development/github.js +40 -43
  37. package/dist/providers/development/github.js.map +1 -1
  38. package/dist/providers/ecommerce/shopify.d.ts.map +1 -1
  39. package/dist/providers/ecommerce/shopify.js +79 -62
  40. package/dist/providers/ecommerce/shopify.js.map +1 -1
  41. package/dist/providers/email/resend.d.ts.map +1 -1
  42. package/dist/providers/email/resend.js +20 -16
  43. package/dist/providers/email/resend.js.map +1 -1
  44. package/dist/providers/email/sendgrid.d.ts.map +1 -1
  45. package/dist/providers/email/sendgrid.js +12 -9
  46. package/dist/providers/email/sendgrid.js.map +1 -1
  47. package/dist/providers/finance/stripe.d.ts.map +1 -1
  48. package/dist/providers/finance/stripe.js +44 -42
  49. package/dist/providers/finance/stripe.js.map +1 -1
  50. package/dist/providers/forms/typeform.d.ts.map +1 -1
  51. package/dist/providers/forms/typeform.js +68 -58
  52. package/dist/providers/forms/typeform.js.map +1 -1
  53. package/dist/providers/knowledge/notion.d.ts.map +1 -1
  54. package/dist/providers/knowledge/notion.js +75 -41
  55. package/dist/providers/knowledge/notion.js.map +1 -1
  56. package/dist/providers/marketing/mailchimp.d.ts.map +1 -1
  57. package/dist/providers/marketing/mailchimp.js +74 -61
  58. package/dist/providers/marketing/mailchimp.js.map +1 -1
  59. package/dist/providers/media/cloudinary.d.ts.map +1 -1
  60. package/dist/providers/media/cloudinary.js +30 -28
  61. package/dist/providers/media/cloudinary.js.map +1 -1
  62. package/dist/providers/messaging/slack.d.ts.map +1 -1
  63. package/dist/providers/messaging/slack.js +75 -58
  64. package/dist/providers/messaging/slack.js.map +1 -1
  65. package/dist/providers/messaging/twilio-sms.d.ts.map +1 -1
  66. package/dist/providers/messaging/twilio-sms.js +33 -15
  67. package/dist/providers/messaging/twilio-sms.js.map +1 -1
  68. package/dist/providers/project-management/linear.d.ts.map +1 -1
  69. package/dist/providers/project-management/linear.js +31 -27
  70. package/dist/providers/project-management/linear.js.map +1 -1
  71. package/dist/providers/spreadsheet/google-sheets.d.ts.map +1 -1
  72. package/dist/providers/spreadsheet/google-sheets.js +21 -18
  73. package/dist/providers/spreadsheet/google-sheets.js.map +1 -1
  74. package/dist/providers/spreadsheet/xlsx.d.ts.map +1 -1
  75. package/dist/providers/spreadsheet/xlsx.js +4 -4
  76. package/dist/providers/spreadsheet/xlsx.js.map +1 -1
  77. package/dist/providers/storage/index.js +1 -0
  78. package/dist/providers/storage/index.js.map +1 -1
  79. package/dist/providers/storage/s3.d.ts.map +1 -1
  80. package/dist/providers/storage/s3.js +36 -27
  81. package/dist/providers/storage/s3.js.map +1 -1
  82. package/dist/providers/support/zendesk.d.ts.map +1 -1
  83. package/dist/providers/support/zendesk.js +24 -25
  84. package/dist/providers/support/zendesk.js.map +1 -1
  85. package/dist/providers/tasks/todoist.d.ts.map +1 -1
  86. package/dist/providers/tasks/todoist.js +18 -18
  87. package/dist/providers/tasks/todoist.js.map +1 -1
  88. package/dist/providers/video-conferencing/google-meet.d.ts.map +1 -1
  89. package/dist/providers/video-conferencing/google-meet.js +11 -11
  90. package/dist/providers/video-conferencing/google-meet.js.map +1 -1
  91. package/dist/providers/video-conferencing/jitsi.js +14 -14
  92. package/dist/providers/video-conferencing/jitsi.js.map +1 -1
  93. package/dist/providers/video-conferencing/teams.d.ts.map +1 -1
  94. package/dist/providers/video-conferencing/teams.js +9 -7
  95. package/dist/providers/video-conferencing/teams.js.map +1 -1
  96. package/dist/providers/video-conferencing/zoom.d.ts.map +1 -1
  97. package/dist/providers/video-conferencing/zoom.js +26 -24
  98. package/dist/providers/video-conferencing/zoom.js.map +1 -1
  99. package/dist/tools/data.d.ts.map +1 -1
  100. package/dist/tools/data.js +5 -12
  101. package/dist/tools/data.js.map +1 -1
  102. package/dist/tools/index.d.ts +1 -0
  103. package/dist/tools/index.d.ts.map +1 -1
  104. package/dist/tools/index.js +1 -0
  105. package/dist/tools/index.js.map +1 -1
  106. package/dist/tools/system.d.ts +289 -0
  107. package/dist/tools/system.d.ts.map +1 -0
  108. package/dist/tools/system.js +752 -0
  109. package/dist/tools/system.js.map +1 -0
  110. package/dist/tools/web.d.ts.map +1 -1
  111. package/dist/tools/web.js +22 -10
  112. package/dist/tools/web.js.map +1 -1
  113. package/dist/track-record.d.ts +101 -0
  114. package/dist/track-record.d.ts.map +1 -0
  115. package/dist/track-record.js +17 -0
  116. package/dist/track-record.js.map +1 -0
  117. package/dist/types.d.ts +210 -9
  118. package/dist/types.d.ts.map +1 -1
  119. package/dist/verb-registration.d.ts +122 -0
  120. package/dist/verb-registration.d.ts.map +1 -0
  121. package/dist/verb-registration.js +176 -0
  122. package/dist/verb-registration.js.map +1 -0
  123. package/dist/worker.d.ts +93 -0
  124. package/dist/worker.d.ts.map +1 -0
  125. package/dist/worker.js +315 -0
  126. package/dist/worker.js.map +1 -0
  127. package/dist/wrap.d.ts +89 -0
  128. package/dist/wrap.d.ts.map +1 -0
  129. package/dist/wrap.js +225 -0
  130. package/dist/wrap.js.map +1 -0
  131. package/package.json +21 -4
  132. package/src/client.ts +136 -0
  133. package/src/define.ts +31 -37
  134. package/src/function-ref.ts +264 -0
  135. package/src/function-sugar.ts +134 -0
  136. package/src/index.ts +132 -10
  137. package/src/providers/analytics/mixpanel.ts +19 -18
  138. package/src/providers/calendar/cal-com.ts +29 -18
  139. package/src/providers/calendar/google-calendar.ts +20 -14
  140. package/src/providers/crm/hubspot.ts +225 -99
  141. package/src/providers/development/github.ts +206 -135
  142. package/src/providers/ecommerce/shopify.ts +250 -89
  143. package/src/providers/email/resend.ts +101 -28
  144. package/src/providers/email/sendgrid.ts +12 -9
  145. package/src/providers/finance/stripe.ts +128 -49
  146. package/src/providers/forms/typeform.ts +74 -58
  147. package/src/providers/knowledge/notion.ts +340 -88
  148. package/src/providers/marketing/mailchimp.ts +86 -70
  149. package/src/providers/media/cloudinary.ts +99 -41
  150. package/src/providers/messaging/slack.ts +283 -85
  151. package/src/providers/messaging/twilio-sms.ts +35 -15
  152. package/src/providers/project-management/linear.ts +143 -55
  153. package/src/providers/spreadsheet/google-sheets.ts +222 -56
  154. package/src/providers/spreadsheet/xlsx.ts +47 -16
  155. package/src/providers/storage/s3.ts +119 -47
  156. package/src/providers/support/zendesk.ts +196 -46
  157. package/src/providers/tasks/todoist.ts +20 -26
  158. package/src/providers/video-conferencing/google-meet.ts +17 -20
  159. package/src/providers/video-conferencing/jitsi.ts +14 -14
  160. package/src/providers/video-conferencing/teams.ts +14 -13
  161. package/src/providers/video-conferencing/zoom.ts +54 -49
  162. package/src/tools/data.ts +6 -16
  163. package/src/tools/index.ts +1 -0
  164. package/src/tools/system.ts +887 -0
  165. package/src/tools/web.ts +22 -10
  166. package/src/track-record.ts +106 -0
  167. package/src/types.ts +241 -13
  168. package/src/verb-registration.ts +197 -0
  169. package/src/worker.ts +370 -0
  170. package/src/wrap.ts +260 -0
  171. package/test/client.test.ts +146 -0
  172. package/test/communication-tools-extended.test.ts +734 -0
  173. package/test/data-tools-extended.test.ts +743 -0
  174. package/test/define-extended.test.ts +819 -0
  175. package/test/define.test.ts +150 -41
  176. package/test/entities.test.ts +623 -0
  177. package/test/extended-entities.test.ts +1228 -0
  178. package/test/provider-implementations.test.ts +725 -0
  179. package/test/provider-registry-extended.test.ts +583 -0
  180. package/test/providers/google-sheets.test.ts +851 -0
  181. package/test/providers/helpers.ts +554 -0
  182. package/test/providers/hubspot.test.ts +576 -0
  183. package/test/providers/slack.test.ts +932 -0
  184. package/test/providers/stripe.test.ts +701 -0
  185. package/test/providers.test.ts +578 -0
  186. package/test/system-tools-extended.test.ts +632 -0
  187. package/test/system.test.ts +673 -0
  188. package/test/tools.test.ts +15 -11
  189. package/test/types.test.ts +402 -0
  190. package/test/verb-registration.test.ts +395 -0
  191. package/test/web-tools.test.ts +553 -0
  192. package/test/worker-extended.test.ts +699 -0
  193. package/test/worker.test.ts +576 -0
  194. package/test/wrap.test.ts +366 -0
  195. package/tsconfig.json +3 -13
  196. package/vitest.config.ts +37 -0
  197. package/wrangler.jsonc +9 -0
  198. package/.turbo/turbo-build.log +0 -5
  199. package/dist/providers/voice/vapi.d.ts +0 -27
  200. package/dist/providers/voice/vapi.d.ts.map +0 -1
  201. package/dist/providers/voice/vapi.js +0 -440
  202. package/dist/providers/voice/vapi.js.map +0 -1
  203. package/src/define.js +0 -267
  204. package/src/entities/advertising.js +0 -999
  205. package/src/entities/ai.js +0 -756
  206. package/src/entities/analytics.js +0 -1588
  207. package/src/entities/automation.js +0 -601
  208. package/src/entities/communication.js +0 -1150
  209. package/src/entities/crm.js +0 -1386
  210. package/src/entities/design.js +0 -546
  211. package/src/entities/development.js +0 -2212
  212. package/src/entities/document.js +0 -874
  213. package/src/entities/ecommerce.js +0 -1429
  214. package/src/entities/experiment.js +0 -1039
  215. package/src/entities/finance.js +0 -3478
  216. package/src/entities/forms.js +0 -1892
  217. package/src/entities/hr.js +0 -661
  218. package/src/entities/identity.js +0 -997
  219. package/src/entities/index.js +0 -282
  220. package/src/entities/infrastructure.js +0 -1153
  221. package/src/entities/knowledge.js +0 -1438
  222. package/src/entities/marketing.js +0 -1610
  223. package/src/entities/media.js +0 -1634
  224. package/src/entities/notification.js +0 -1199
  225. package/src/entities/presentation.js +0 -1274
  226. package/src/entities/productivity.js +0 -1317
  227. package/src/entities/project-management.js +0 -1136
  228. package/src/entities/recruiting.js +0 -736
  229. package/src/entities/shipping.js +0 -509
  230. package/src/entities/signature.js +0 -1102
  231. package/src/entities/site.js +0 -222
  232. package/src/entities/spreadsheet.js +0 -1341
  233. package/src/entities/storage.js +0 -1198
  234. package/src/entities/support.js +0 -1166
  235. package/src/entities/video-conferencing.js +0 -1750
  236. package/src/entities/video.js +0 -950
  237. package/src/entities.js +0 -1663
  238. package/src/index.js +0 -74
  239. package/src/providers/analytics/index.js +0 -17
  240. package/src/providers/analytics/mixpanel.js +0 -255
  241. package/src/providers/calendar/cal-com.js +0 -303
  242. package/src/providers/calendar/google-calendar.js +0 -335
  243. package/src/providers/calendar/index.js +0 -20
  244. package/src/providers/crm/hubspot.js +0 -566
  245. package/src/providers/crm/index.js +0 -17
  246. package/src/providers/development/github.js +0 -472
  247. package/src/providers/development/index.js +0 -17
  248. package/src/providers/ecommerce/index.js +0 -17
  249. package/src/providers/ecommerce/shopify.js +0 -378
  250. package/src/providers/email/index.js +0 -20
  251. package/src/providers/email/resend.js +0 -258
  252. package/src/providers/email/sendgrid.js +0 -161
  253. package/src/providers/finance/index.js +0 -17
  254. package/src/providers/finance/stripe.js +0 -549
  255. package/src/providers/forms/index.js +0 -17
  256. package/src/providers/forms/typeform.js +0 -500
  257. package/src/providers/index.js +0 -123
  258. package/src/providers/knowledge/index.js +0 -17
  259. package/src/providers/knowledge/notion.js +0 -389
  260. package/src/providers/marketing/index.js +0 -17
  261. package/src/providers/marketing/mailchimp.js +0 -443
  262. package/src/providers/media/cloudinary.js +0 -318
  263. package/src/providers/media/index.js +0 -17
  264. package/src/providers/messaging/index.js +0 -20
  265. package/src/providers/messaging/slack.js +0 -393
  266. package/src/providers/messaging/twilio-sms.js +0 -249
  267. package/src/providers/project-management/index.js +0 -17
  268. package/src/providers/project-management/linear.js +0 -575
  269. package/src/providers/registry.js +0 -86
  270. package/src/providers/spreadsheet/google-sheets.js +0 -375
  271. package/src/providers/spreadsheet/index.js +0 -20
  272. package/src/providers/spreadsheet/xlsx.js +0 -423
  273. package/src/providers/storage/index.js +0 -24
  274. package/src/providers/storage/s3.js +0 -419
  275. package/src/providers/support/index.js +0 -17
  276. package/src/providers/support/zendesk.js +0 -373
  277. package/src/providers/tasks/index.js +0 -17
  278. package/src/providers/tasks/todoist.js +0 -286
  279. package/src/providers/types.js +0 -9
  280. package/src/providers/video-conferencing/google-meet.js +0 -286
  281. package/src/providers/video-conferencing/index.js +0 -31
  282. package/src/providers/video-conferencing/jitsi.js +0 -254
  283. package/src/providers/video-conferencing/teams.js +0 -270
  284. package/src/providers/video-conferencing/zoom.js +0 -332
  285. package/src/registry.js +0 -128
  286. package/src/tools/communication.js +0 -184
  287. package/src/tools/data.js +0 -205
  288. package/src/tools/index.js +0 -11
  289. package/src/tools/web.js +0 -137
  290. package/src/types.js +0 -10
  291. package/test/define.test.js +0 -306
  292. package/test/registry.test.js +0 -357
  293. 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
+ })