digital-workers 2.1.3 → 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 (183) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +2 -0
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +33 -21
  5. package/dist/actions.js.map +1 -1
  6. package/dist/agent-comms.d.ts.map +1 -1
  7. package/dist/agent-comms.js +36 -25
  8. package/dist/agent-comms.js.map +1 -1
  9. package/dist/approve.d.ts +40 -8
  10. package/dist/approve.d.ts.map +1 -1
  11. package/dist/approve.js +86 -20
  12. package/dist/approve.js.map +1 -1
  13. package/dist/ask.d.ts +38 -7
  14. package/dist/ask.d.ts.map +1 -1
  15. package/dist/ask.js +85 -25
  16. package/dist/ask.js.map +1 -1
  17. package/dist/browse.d.ts +223 -0
  18. package/dist/browse.d.ts.map +1 -0
  19. package/dist/browse.js +392 -0
  20. package/dist/browse.js.map +1 -0
  21. package/dist/capability-tiers.js +3 -3
  22. package/dist/capability-tiers.js.map +1 -1
  23. package/dist/cascade-context.d.ts +28 -28
  24. package/dist/client.d.ts +162 -0
  25. package/dist/client.d.ts.map +1 -0
  26. package/dist/client.js +64 -0
  27. package/dist/client.js.map +1 -0
  28. package/dist/decide.d.ts +42 -6
  29. package/dist/decide.d.ts.map +1 -1
  30. package/dist/decide.js +54 -11
  31. package/dist/decide.js.map +1 -1
  32. package/dist/do.d.ts +36 -7
  33. package/dist/do.d.ts.map +1 -1
  34. package/dist/do.js +82 -39
  35. package/dist/do.js.map +1 -1
  36. package/dist/error-escalation.d.ts.map +1 -1
  37. package/dist/error-escalation.js +38 -38
  38. package/dist/error-escalation.js.map +1 -1
  39. package/dist/generate.d.ts +48 -7
  40. package/dist/generate.d.ts.map +1 -1
  41. package/dist/generate.js +49 -8
  42. package/dist/generate.js.map +1 -1
  43. package/dist/goals.d.ts +10 -9
  44. package/dist/goals.d.ts.map +1 -1
  45. package/dist/goals.js +30 -24
  46. package/dist/goals.js.map +1 -1
  47. package/dist/image.d.ts +189 -0
  48. package/dist/image.d.ts.map +1 -0
  49. package/dist/image.js +528 -0
  50. package/dist/image.js.map +1 -0
  51. package/dist/index.d.ts +49 -2
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +58 -2
  54. package/dist/index.js.map +1 -1
  55. package/dist/is.d.ts +45 -10
  56. package/dist/is.d.ts.map +1 -1
  57. package/dist/is.js +56 -21
  58. package/dist/is.js.map +1 -1
  59. package/dist/kpis.d.ts +24 -15
  60. package/dist/kpis.d.ts.map +1 -1
  61. package/dist/kpis.js +16 -14
  62. package/dist/kpis.js.map +1 -1
  63. package/dist/load-balancing.d.ts.map +1 -1
  64. package/dist/load-balancing.js +124 -38
  65. package/dist/load-balancing.js.map +1 -1
  66. package/dist/logger.d.ts +76 -0
  67. package/dist/logger.d.ts.map +1 -0
  68. package/dist/logger.js +39 -0
  69. package/dist/logger.js.map +1 -0
  70. package/dist/notify.d.ts +38 -9
  71. package/dist/notify.d.ts.map +1 -1
  72. package/dist/notify.js +72 -17
  73. package/dist/notify.js.map +1 -1
  74. package/dist/role.d.ts +5 -4
  75. package/dist/role.d.ts.map +1 -1
  76. package/dist/role.js +13 -10
  77. package/dist/role.js.map +1 -1
  78. package/dist/runtime.d.ts +310 -0
  79. package/dist/runtime.d.ts.map +1 -0
  80. package/dist/runtime.js +510 -0
  81. package/dist/runtime.js.map +1 -0
  82. package/dist/team.d.ts +11 -6
  83. package/dist/team.d.ts.map +1 -1
  84. package/dist/team.js +22 -15
  85. package/dist/team.js.map +1 -1
  86. package/dist/transports/email.d.ts +318 -0
  87. package/dist/transports/email.d.ts.map +1 -0
  88. package/dist/transports/email.js +779 -0
  89. package/dist/transports/email.js.map +1 -0
  90. package/dist/transports/slack.d.ts +515 -0
  91. package/dist/transports/slack.d.ts.map +1 -0
  92. package/dist/transports/slack.js +844 -0
  93. package/dist/transports/slack.js.map +1 -0
  94. package/dist/transports.d.ts.map +1 -1
  95. package/dist/transports.js +44 -25
  96. package/dist/transports.js.map +1 -1
  97. package/dist/types.d.ts +141 -19
  98. package/dist/types.d.ts.map +1 -1
  99. package/dist/types.js +5 -0
  100. package/dist/types.js.map +1 -1
  101. package/dist/utils/id.d.ts +19 -0
  102. package/dist/utils/id.d.ts.map +1 -0
  103. package/dist/utils/id.js +21 -0
  104. package/dist/utils/id.js.map +1 -0
  105. package/dist/video.d.ts +203 -0
  106. package/dist/video.d.ts.map +1 -0
  107. package/dist/video.js +528 -0
  108. package/dist/video.js.map +1 -0
  109. package/dist/worker.d.ts +343 -0
  110. package/dist/worker.d.ts.map +1 -0
  111. package/dist/worker.js +698 -0
  112. package/dist/worker.js.map +1 -0
  113. package/package.json +32 -14
  114. package/src/actions.ts +39 -30
  115. package/src/agent-comms.ts +54 -92
  116. package/src/approve.ts +91 -20
  117. package/src/ask.ts +99 -25
  118. package/src/browse.ts +627 -0
  119. package/src/capability-tiers.ts +5 -5
  120. package/src/client.ts +221 -0
  121. package/src/decide.ts +81 -35
  122. package/src/do.ts +98 -52
  123. package/src/error-escalation.ts +55 -67
  124. package/src/generate.ts +52 -18
  125. package/src/goals.ts +36 -27
  126. package/src/image.ts +816 -0
  127. package/src/index.ts +187 -2
  128. package/src/is.ts +59 -25
  129. package/src/kpis.ts +41 -36
  130. package/src/load-balancing.ts +132 -46
  131. package/src/logger.ts +93 -0
  132. package/src/notify.ts +78 -17
  133. package/src/role.ts +30 -20
  134. package/src/runtime.ts +796 -0
  135. package/src/team.ts +24 -19
  136. package/src/transports/email.ts +1160 -0
  137. package/src/transports/slack.ts +1320 -0
  138. package/src/transports.ts +58 -43
  139. package/src/types.ts +174 -46
  140. package/src/utils/id.ts +21 -0
  141. package/src/video.ts +906 -0
  142. package/src/worker.ts +1007 -0
  143. package/test/approve.test.ts +305 -0
  144. package/test/ask.test.ts +274 -0
  145. package/test/browse.test.ts +361 -0
  146. package/test/decide.test.ts +252 -0
  147. package/test/do.test.ts +144 -0
  148. package/test/error-logging.test.ts +357 -0
  149. package/test/generate.test.ts +319 -0
  150. package/test/image.test.ts +398 -0
  151. package/test/is.test.ts +287 -0
  152. package/test/load-balancing-safety.test.ts +404 -0
  153. package/test/notify.test.ts +434 -0
  154. package/test/primitives.test.ts +320 -0
  155. package/test/runtime-integration.test.ts +892 -0
  156. package/test/transports/crypto.test.ts +230 -0
  157. package/test/transports/email.test.ts +866 -0
  158. package/test/transports/id-generation.test.ts +91 -0
  159. package/test/transports/slack.test.ts +760 -0
  160. package/test/type-safety.test.ts +834 -0
  161. package/test/types.test.ts +60 -2
  162. package/test/video.test.ts +530 -0
  163. package/test/worker.test.ts +1433 -0
  164. package/tsconfig.json +4 -1
  165. package/vitest.config.ts +42 -0
  166. package/wrangler.jsonc +36 -0
  167. package/.turbo/turbo-build.log +0 -4
  168. package/LICENSE +0 -21
  169. package/src/actions.js +0 -436
  170. package/src/approve.js +0 -234
  171. package/src/ask.js +0 -226
  172. package/src/decide.js +0 -244
  173. package/src/do.js +0 -227
  174. package/src/generate.js +0 -298
  175. package/src/goals.js +0 -205
  176. package/src/index.js +0 -68
  177. package/src/is.js +0 -317
  178. package/src/kpis.js +0 -270
  179. package/src/notify.js +0 -219
  180. package/src/role.js +0 -110
  181. package/src/team.js +0 -130
  182. package/src/transports.js +0 -357
  183. package/src/types.js +0 -71
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Tests for do() - Task execution primitive
3
+ *
4
+ * The do() function routes tasks to appropriate Workers (AI Agents or Humans)
5
+ * based on capability matching. Unlike ai-functions.do() which directly calls
6
+ * the LLM, this function provides worker coordination with retries and timeouts.
7
+ *
8
+ * These tests use real AI calls via the Cloudflare AI Gateway.
9
+ * Tests are skipped if AI_GATEWAY_URL is not configured.
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest'
13
+ import { do as doTask } from '../src/index.js'
14
+
15
+ // Skip tests if no gateway configured
16
+ const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
17
+
18
+ describe('do() - Task Execution Primitive', () => {
19
+ describe('Unit Tests (no AI)', () => {
20
+ it('should be exported from index', () => {
21
+ expect(doTask).toBeDefined()
22
+ expect(typeof doTask).toBe('function')
23
+ })
24
+
25
+ it('should have parallel method', () => {
26
+ expect(doTask.parallel).toBeDefined()
27
+ expect(typeof doTask.parallel).toBe('function')
28
+ })
29
+
30
+ it('should have sequence method', () => {
31
+ expect(doTask.sequence).toBeDefined()
32
+ expect(typeof doTask.sequence).toBe('function')
33
+ })
34
+
35
+ it('should have withDependencies method', () => {
36
+ expect(doTask.withDependencies).toBeDefined()
37
+ expect(typeof doTask.withDependencies).toBe('function')
38
+ })
39
+ })
40
+
41
+ describe.skipIf(!hasGateway)('Integration Tests (with AI)', () => {
42
+ it('should execute a simple task', async () => {
43
+ const result = await doTask('Calculate 2 + 2 and return the result', {
44
+ timeout: 30000,
45
+ })
46
+
47
+ expect(result).toBeDefined()
48
+ expect(typeof result.success).toBe('boolean')
49
+ expect(typeof result.duration).toBe('number')
50
+ expect(result.duration).toBeGreaterThan(0)
51
+ })
52
+
53
+ it('should execute task with context', async () => {
54
+ const result = await doTask('Summarize the provided text', {
55
+ timeout: 30000,
56
+ context: {
57
+ text: 'Hello world. This is a simple test message.',
58
+ },
59
+ })
60
+
61
+ expect(result).toBeDefined()
62
+ expect(result.success).toBeDefined()
63
+ expect(result.duration).toBeGreaterThan(0)
64
+ })
65
+
66
+ it('should return steps array', async () => {
67
+ const result = await doTask('List 3 colors', {
68
+ timeout: 30000,
69
+ })
70
+
71
+ expect(result).toBeDefined()
72
+ expect(Array.isArray(result.steps)).toBe(true)
73
+ })
74
+
75
+ it('should handle task with structured context', async () => {
76
+ const result = await doTask<{ name: string; greeting: string }>(
77
+ 'Generate a greeting for the user',
78
+ {
79
+ timeout: 30000,
80
+ context: {
81
+ user: { name: 'Alice', language: 'English' },
82
+ },
83
+ }
84
+ )
85
+
86
+ expect(result).toBeDefined()
87
+ expect(result.success).toBeDefined()
88
+ })
89
+
90
+ it('should handle timeout option', async () => {
91
+ // Very short timeout to test timeout behavior
92
+ const result = await doTask('Count from 1 to 5', {
93
+ timeout: 1, // 1ms timeout - will likely fail
94
+ })
95
+
96
+ // Either succeeds quickly or fails due to timeout
97
+ expect(result).toBeDefined()
98
+ expect(typeof result.success).toBe('boolean')
99
+ })
100
+
101
+ it('should execute parallel tasks', async () => {
102
+ const results = await doTask.parallel(['What is 1+1?', 'What is 2+2?', 'What is 3+3?'], {
103
+ timeout: 30000,
104
+ })
105
+
106
+ expect(results).toBeDefined()
107
+ expect(Array.isArray(results)).toBe(true)
108
+ expect(results.length).toBe(3)
109
+ results.forEach((result) => {
110
+ expect(result.success).toBeDefined()
111
+ expect(result.duration).toBeDefined()
112
+ })
113
+ })
114
+
115
+ it('should execute sequential tasks', async () => {
116
+ const results = await doTask.sequence(['Say hello', 'Say goodbye'], { timeout: 30000 })
117
+
118
+ expect(results).toBeDefined()
119
+ expect(Array.isArray(results)).toBe(true)
120
+ expect(results.length).toBe(2)
121
+ })
122
+
123
+ it('should handle task with retries option', async () => {
124
+ const result = await doTask('Return the word "success"', {
125
+ maxRetries: 1,
126
+ timeout: 30000,
127
+ })
128
+
129
+ expect(result).toBeDefined()
130
+ expect(result.success).toBeDefined()
131
+ })
132
+
133
+ it('should support background execution option', async () => {
134
+ const result = await doTask('Generate a random number', {
135
+ background: true,
136
+ timeout: 30000,
137
+ })
138
+
139
+ expect(result).toBeDefined()
140
+ // Background tasks still return a result
141
+ expect(result.success).toBeDefined()
142
+ })
143
+ })
144
+ })
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Error Logging Tests
3
+ *
4
+ * TDD tests verifying that catch blocks log errors instead of silently returning null.
5
+ * Following Red-Green-Refactor methodology.
6
+ *
7
+ * Bead issue: aip-prsc
8
+ */
9
+
10
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
11
+ import type { Logger } from '../src/logger.js'
12
+ import { createSlackTransport } from '../src/transports/slack.js'
13
+ import type { SlackTransportConfig } from '../src/transports/slack.js'
14
+
15
+ // ============================================================================
16
+ // Test Fixtures
17
+ // ============================================================================
18
+
19
+ /**
20
+ * Creates a mock logger for testing
21
+ */
22
+ function createMockLogger(): Logger & {
23
+ calls: {
24
+ debug: Array<{ msg: string; meta?: object }>
25
+ info: Array<{ msg: string; meta?: object }>
26
+ warn: Array<{ msg: string; meta?: object }>
27
+ error: Array<{ msg: string; error?: Error; meta?: object }>
28
+ }
29
+ } {
30
+ const calls = {
31
+ debug: [] as Array<{ msg: string; meta?: object }>,
32
+ info: [] as Array<{ msg: string; meta?: object }>,
33
+ warn: [] as Array<{ msg: string; meta?: object }>,
34
+ error: [] as Array<{ msg: string; error?: Error; meta?: object }>,
35
+ }
36
+
37
+ return {
38
+ calls,
39
+ debug(msg: string, meta?: object) {
40
+ calls.debug.push({ msg, meta })
41
+ },
42
+ info(msg: string, meta?: object) {
43
+ calls.info.push({ msg, meta })
44
+ },
45
+ warn(msg: string, meta?: object) {
46
+ calls.warn.push({ msg, meta })
47
+ },
48
+ error(msg: string, error?: Error, meta?: object) {
49
+ calls.error.push({ msg, error, meta })
50
+ },
51
+ }
52
+ }
53
+
54
+ // Test configuration
55
+ const testConfig: Omit<SlackTransportConfig, 'transport'> = {
56
+ botToken: 'xoxb-test-token',
57
+ signingSecret: 'test-signing-secret',
58
+ apiUrl: 'https://slack.test/api',
59
+ }
60
+
61
+ // ============================================================================
62
+ // Slack Transport Error Logging Tests
63
+ // ============================================================================
64
+
65
+ describe('SlackTransport Error Logging', () => {
66
+ let mockLogger: ReturnType<typeof createMockLogger>
67
+ let mockFetch: ReturnType<typeof vi.fn>
68
+ let originalFetch: typeof globalThis.fetch
69
+
70
+ beforeEach(() => {
71
+ mockLogger = createMockLogger()
72
+ mockFetch = vi.fn()
73
+ originalFetch = globalThis.fetch
74
+ globalThis.fetch = mockFetch
75
+ })
76
+
77
+ afterEach(() => {
78
+ globalThis.fetch = originalFetch
79
+ vi.restoreAllMocks()
80
+ })
81
+
82
+ describe('lookupUserByEmail', () => {
83
+ it('should log error when API call throws', async () => {
84
+ // Arrange: Make fetch throw an error
85
+ const apiError = new Error('Network error')
86
+ mockFetch.mockRejectedValue(apiError)
87
+
88
+ const transport = createSlackTransport({
89
+ ...testConfig,
90
+ logger: mockLogger,
91
+ })
92
+
93
+ // Act
94
+ const result = await transport.lookupUserByEmail('test@example.com')
95
+
96
+ // Assert: Original behavior preserved
97
+ expect(result).toBeNull()
98
+
99
+ // Assert: Error was logged with context
100
+ expect(mockLogger.calls.error.length).toBe(1)
101
+ expect(mockLogger.calls.error[0].msg).toContain('lookupUserByEmail')
102
+ expect(mockLogger.calls.error[0].error).toBe(apiError)
103
+ expect(mockLogger.calls.error[0].meta).toMatchObject({
104
+ email: 'test@example.com',
105
+ })
106
+ })
107
+
108
+ it('should not log when no error occurs', async () => {
109
+ // Arrange: Successful API response
110
+ mockFetch.mockResolvedValue({
111
+ ok: true,
112
+ json: () => Promise.resolve({ ok: true, user: { id: 'U123' } }),
113
+ })
114
+
115
+ const transport = createSlackTransport({
116
+ ...testConfig,
117
+ logger: mockLogger,
118
+ })
119
+
120
+ // Act
121
+ const result = await transport.lookupUserByEmail('test@example.com')
122
+
123
+ // Assert
124
+ expect(result).toBe('U123')
125
+ expect(mockLogger.calls.error.length).toBe(0)
126
+ })
127
+
128
+ it('should work without logger (backward compatibility)', async () => {
129
+ // Arrange: Make fetch throw an error
130
+ mockFetch.mockRejectedValue(new Error('Network error'))
131
+
132
+ const transport = createSlackTransport(testConfig)
133
+
134
+ // Act & Assert: Should not throw when no logger provided
135
+ const result = await transport.lookupUserByEmail('test@example.com')
136
+ expect(result).toBeNull()
137
+ })
138
+ })
139
+
140
+ describe('parseWebhookPayload', () => {
141
+ it('should log error when JSON parsing fails', () => {
142
+ // Arrange
143
+ const transport = createSlackTransport({
144
+ ...testConfig,
145
+ logger: mockLogger,
146
+ })
147
+
148
+ // Create invalid JSON payload
149
+ const invalidRequest = {
150
+ body: 'payload={invalid-json}',
151
+ headers: {},
152
+ timestamp: Date.now().toString(),
153
+ signature: 'v0=invalid',
154
+ }
155
+
156
+ // Act
157
+ const result = transport.parseWebhookPayloadForTesting(invalidRequest)
158
+
159
+ // Assert: Original behavior preserved
160
+ expect(result).toBeNull()
161
+
162
+ // Assert: Error was logged
163
+ expect(mockLogger.calls.error.length).toBe(1)
164
+ expect(mockLogger.calls.error[0].msg).toContain('parseWebhookPayload')
165
+ expect(mockLogger.calls.error[0].error).toBeInstanceOf(SyntaxError)
166
+ })
167
+
168
+ it('should not log when parsing succeeds', () => {
169
+ // Arrange
170
+ const transport = createSlackTransport({
171
+ ...testConfig,
172
+ logger: mockLogger,
173
+ })
174
+
175
+ const validPayload = { type: 'block_actions', user: { id: 'U123' } }
176
+ const validRequest = {
177
+ body: `payload=${encodeURIComponent(JSON.stringify(validPayload))}`,
178
+ headers: {},
179
+ timestamp: Date.now().toString(),
180
+ signature: 'v0=test',
181
+ }
182
+
183
+ // Act
184
+ const result = transport.parseWebhookPayloadForTesting(validRequest)
185
+
186
+ // Assert
187
+ expect(result).toEqual(validPayload)
188
+ expect(mockLogger.calls.error.length).toBe(0)
189
+ })
190
+
191
+ it('should work without logger (backward compatibility)', () => {
192
+ // Arrange
193
+ const transport = createSlackTransport(testConfig)
194
+
195
+ const invalidRequest = {
196
+ body: 'payload={invalid-json}',
197
+ headers: {},
198
+ timestamp: Date.now().toString(),
199
+ signature: 'v0=invalid',
200
+ }
201
+
202
+ // Act & Assert: Should not throw when no logger provided
203
+ const result = transport.parseWebhookPayloadForTesting(invalidRequest)
204
+ expect(result).toBeNull()
205
+ })
206
+ })
207
+
208
+ describe('parseActionValue', () => {
209
+ it('should log debug when JSON parsing fails (expected for string values)', () => {
210
+ // Arrange
211
+ const transport = createSlackTransport({
212
+ ...testConfig,
213
+ logger: mockLogger,
214
+ })
215
+
216
+ // Act: Parse a plain string (not JSON)
217
+ const result = transport.parseActionValueForTesting('plain-string-value')
218
+
219
+ // Assert: Original behavior preserved (returns original value)
220
+ expect(result).toBe('plain-string-value')
221
+
222
+ // Assert: Debug was logged (this is expected behavior, not an error)
223
+ expect(mockLogger.calls.debug.length).toBe(1)
224
+ expect(mockLogger.calls.debug[0].msg).toContain('parseActionValue')
225
+ })
226
+
227
+ it('should not log when JSON parsing succeeds', () => {
228
+ // Arrange
229
+ const transport = createSlackTransport({
230
+ ...testConfig,
231
+ logger: mockLogger,
232
+ })
233
+
234
+ const jsonValue = { action: 'approve', data: 123 }
235
+
236
+ // Act
237
+ const result = transport.parseActionValueForTesting(JSON.stringify(jsonValue))
238
+
239
+ // Assert
240
+ expect(result).toEqual(jsonValue)
241
+ expect(mockLogger.calls.debug.length).toBe(0)
242
+ })
243
+ })
244
+ })
245
+
246
+ // ============================================================================
247
+ // Logger Interface Tests
248
+ // ============================================================================
249
+
250
+ describe('Logger Interface', () => {
251
+ it('should define all required log levels', async () => {
252
+ // Import the Logger interface and verify it has all required methods
253
+ const { Logger } = await import('../src/logger.js')
254
+
255
+ // This test verifies the interface exists - actual type checking is done by TypeScript
256
+ const mockLogger: Logger = {
257
+ debug: vi.fn(),
258
+ info: vi.fn(),
259
+ warn: vi.fn(),
260
+ error: vi.fn(),
261
+ }
262
+
263
+ // Verify methods are callable
264
+ mockLogger.debug('test debug')
265
+ mockLogger.info('test info', { key: 'value' })
266
+ mockLogger.warn('test warn')
267
+ mockLogger.error('test error', new Error('test'), { context: 'test' })
268
+
269
+ expect(mockLogger.debug).toHaveBeenCalledTimes(1)
270
+ expect(mockLogger.info).toHaveBeenCalledTimes(1)
271
+ expect(mockLogger.warn).toHaveBeenCalledTimes(1)
272
+ expect(mockLogger.error).toHaveBeenCalledTimes(1)
273
+ })
274
+
275
+ it('should allow optional meta parameter', async () => {
276
+ const { Logger } = await import('../src/logger.js')
277
+
278
+ const mockLogger: Logger = {
279
+ debug: vi.fn(),
280
+ info: vi.fn(),
281
+ warn: vi.fn(),
282
+ error: vi.fn(),
283
+ }
284
+
285
+ // All methods should work without meta
286
+ mockLogger.debug('no meta')
287
+ mockLogger.info('no meta')
288
+ mockLogger.warn('no meta')
289
+ mockLogger.error('no meta')
290
+
291
+ // Error method should work with error but no meta
292
+ mockLogger.error('with error', new Error('test'))
293
+
294
+ expect(mockLogger.debug).toHaveBeenCalledWith('no meta')
295
+ expect(mockLogger.error).toHaveBeenCalledWith('with error', new Error('test'))
296
+ })
297
+ })
298
+
299
+ // ============================================================================
300
+ // Error Context Preservation Tests
301
+ // ============================================================================
302
+
303
+ describe('Error Context Preservation', () => {
304
+ let mockLogger: ReturnType<typeof createMockLogger>
305
+ let mockFetch: ReturnType<typeof vi.fn>
306
+ let originalFetch: typeof globalThis.fetch
307
+
308
+ beforeEach(() => {
309
+ mockLogger = createMockLogger()
310
+ mockFetch = vi.fn()
311
+ originalFetch = globalThis.fetch
312
+ globalThis.fetch = mockFetch
313
+ })
314
+
315
+ afterEach(() => {
316
+ globalThis.fetch = originalFetch
317
+ })
318
+
319
+ it('should preserve error stack trace', async () => {
320
+ // Arrange
321
+ const apiError = new Error('API failure')
322
+ mockFetch.mockRejectedValue(apiError)
323
+
324
+ const transport = createSlackTransport({
325
+ ...testConfig,
326
+ logger: mockLogger,
327
+ })
328
+
329
+ // Act
330
+ await transport.lookupUserByEmail('test@example.com')
331
+
332
+ // Assert: Error object with stack trace is passed to logger
333
+ const loggedError = mockLogger.calls.error[0].error
334
+ expect(loggedError).toBe(apiError)
335
+ expect(loggedError?.stack).toBeDefined()
336
+ })
337
+
338
+ it('should include operation context in log metadata', async () => {
339
+ // Arrange
340
+ mockFetch.mockRejectedValue(new Error('Network error'))
341
+
342
+ const transport = createSlackTransport({
343
+ ...testConfig,
344
+ logger: mockLogger,
345
+ })
346
+
347
+ // Act
348
+ await transport.lookupUserByEmail('user@example.com')
349
+
350
+ // Assert: Context metadata is included
351
+ const meta = mockLogger.calls.error[0].meta
352
+ expect(meta).toMatchObject({
353
+ email: 'user@example.com',
354
+ operation: 'lookupUserByEmail',
355
+ })
356
+ })
357
+ })