digital-workers 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.
- package/CHANGELOG.md +23 -0
- package/README.md +136 -180
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +34 -21
- package/dist/actions.js.map +1 -1
- package/dist/agent-comms.d.ts +438 -0
- package/dist/agent-comms.d.ts.map +1 -0
- package/dist/agent-comms.js +677 -0
- package/dist/agent-comms.js.map +1 -0
- package/dist/approve.d.ts +40 -8
- package/dist/approve.d.ts.map +1 -1
- package/dist/approve.js +86 -20
- package/dist/approve.js.map +1 -1
- package/dist/ask.d.ts +38 -7
- package/dist/ask.d.ts.map +1 -1
- package/dist/ask.js +85 -25
- package/dist/ask.js.map +1 -1
- package/dist/browse.d.ts +223 -0
- package/dist/browse.d.ts.map +1 -0
- package/dist/browse.js +392 -0
- package/dist/browse.js.map +1 -0
- package/dist/capability-tiers.d.ts +230 -0
- package/dist/capability-tiers.d.ts.map +1 -0
- package/dist/capability-tiers.js +388 -0
- package/dist/capability-tiers.js.map +1 -0
- package/dist/cascade-context.d.ts +523 -0
- package/dist/cascade-context.d.ts.map +1 -0
- package/dist/cascade-context.js +494 -0
- package/dist/cascade-context.js.map +1 -0
- package/dist/client.d.ts +162 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +64 -0
- package/dist/client.js.map +1 -0
- package/dist/decide.d.ts +42 -6
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +54 -11
- package/dist/decide.js.map +1 -1
- package/dist/do.d.ts +36 -7
- package/dist/do.d.ts.map +1 -1
- package/dist/do.js +82 -39
- package/dist/do.js.map +1 -1
- package/dist/error-escalation.d.ts +416 -0
- package/dist/error-escalation.d.ts.map +1 -0
- package/dist/error-escalation.js +656 -0
- package/dist/error-escalation.js.map +1 -0
- package/dist/generate.d.ts +48 -7
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +49 -8
- package/dist/generate.js.map +1 -1
- package/dist/goals.d.ts +10 -9
- package/dist/goals.d.ts.map +1 -1
- package/dist/goals.js +30 -24
- package/dist/goals.js.map +1 -1
- package/dist/image.d.ts +189 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +528 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +59 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +92 -2
- package/dist/index.js.map +1 -1
- package/dist/is.d.ts +45 -10
- package/dist/is.d.ts.map +1 -1
- package/dist/is.js +56 -21
- package/dist/is.js.map +1 -1
- package/dist/kpis.d.ts +24 -15
- package/dist/kpis.d.ts.map +1 -1
- package/dist/kpis.js +16 -14
- package/dist/kpis.js.map +1 -1
- package/dist/load-balancing.d.ts +395 -0
- package/dist/load-balancing.d.ts.map +1 -0
- package/dist/load-balancing.js +991 -0
- package/dist/load-balancing.js.map +1 -0
- package/dist/logger.d.ts +76 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +39 -0
- package/dist/logger.js.map +1 -0
- package/dist/notify.d.ts +38 -9
- package/dist/notify.d.ts.map +1 -1
- package/dist/notify.js +72 -17
- package/dist/notify.js.map +1 -1
- package/dist/role.d.ts +5 -4
- package/dist/role.d.ts.map +1 -1
- package/dist/role.js +13 -10
- package/dist/role.js.map +1 -1
- package/dist/runtime.d.ts +310 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +510 -0
- package/dist/runtime.js.map +1 -0
- package/dist/team.d.ts +11 -6
- package/dist/team.d.ts.map +1 -1
- package/dist/team.js +22 -15
- package/dist/team.js.map +1 -1
- package/dist/transports/email.d.ts +318 -0
- package/dist/transports/email.d.ts.map +1 -0
- package/dist/transports/email.js +779 -0
- package/dist/transports/email.js.map +1 -0
- package/dist/transports/slack.d.ts +515 -0
- package/dist/transports/slack.d.ts.map +1 -0
- package/dist/transports/slack.js +844 -0
- package/dist/transports/slack.js.map +1 -0
- package/dist/transports.d.ts.map +1 -1
- package/dist/transports.js +44 -25
- package/dist/transports.js.map +1 -1
- package/dist/types.d.ts +149 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/utils/id.d.ts +19 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +21 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/video.d.ts +203 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +528 -0
- package/dist/video.js.map +1 -0
- package/dist/worker.d.ts +343 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +698 -0
- package/dist/worker.js.map +1 -0
- package/package.json +24 -5
- package/src/actions.ts +48 -38
- package/src/agent-comms.ts +1200 -0
- package/src/approve.ts +91 -20
- package/src/ask.ts +99 -25
- package/src/browse.ts +627 -0
- package/src/capability-tiers.ts +545 -0
- package/src/cascade-context.ts +648 -0
- package/src/client.ts +221 -0
- package/src/decide.ts +81 -35
- package/src/do.ts +98 -52
- package/src/error-escalation.ts +1123 -0
- package/src/generate.ts +52 -18
- package/src/goals.ts +36 -27
- package/src/image.ts +816 -0
- package/src/index.ts +410 -2
- package/src/is.ts +59 -25
- package/src/kpis.ts +41 -36
- package/src/load-balancing.ts +1467 -0
- package/src/logger.ts +93 -0
- package/src/notify.ts +78 -17
- package/src/role.ts +30 -20
- package/src/runtime.ts +796 -0
- package/src/team.ts +24 -19
- package/src/transports/email.ts +1160 -0
- package/src/transports/slack.ts +1320 -0
- package/src/transports.ts +58 -43
- package/src/types.ts +182 -46
- package/src/utils/id.ts +21 -0
- package/src/video.ts +906 -0
- package/src/worker.ts +1007 -0
- package/test/agent-comms.test.ts +1397 -0
- package/test/approve.test.ts +305 -0
- package/test/ask.test.ts +274 -0
- package/test/browse.test.ts +361 -0
- package/test/capability-tiers.test.ts +631 -0
- package/test/cascade-context.test.ts +692 -0
- package/test/decide.test.ts +252 -0
- package/test/do.test.ts +144 -0
- package/test/error-escalation.test.ts +1205 -0
- package/test/error-logging.test.ts +357 -0
- package/test/generate.test.ts +319 -0
- package/test/image.test.ts +398 -0
- package/test/is.test.ts +287 -0
- package/test/load-balancing-safety.test.ts +404 -0
- package/test/load-balancing-thread-safety.test.ts +464 -0
- package/test/load-balancing.test.ts +1145 -0
- package/test/notify.test.ts +434 -0
- package/test/primitives.test.ts +320 -0
- package/test/runtime-integration.test.ts +892 -0
- package/test/transports/crypto.test.ts +230 -0
- package/test/transports/email.test.ts +866 -0
- package/test/transports/id-generation.test.ts +91 -0
- package/test/transports/slack.test.ts +760 -0
- package/test/type-safety.test.ts +834 -0
- package/test/types.test.ts +95 -2
- package/test/video.test.ts +530 -0
- package/test/worker.test.ts +1433 -0
- package/tsconfig.json +4 -1
- package/vitest.config.ts +42 -0
- package/wrangler.jsonc +36 -0
- package/.turbo/turbo-build.log +0 -5
- package/src/actions.js +0 -436
- package/src/approve.js +0 -234
- package/src/ask.js +0 -226
- package/src/decide.js +0 -244
- package/src/do.js +0 -227
- package/src/generate.js +0 -298
- package/src/goals.js +0 -205
- package/src/index.js +0 -68
- package/src/is.js +0 -317
- package/src/kpis.js +0 -270
- package/src/notify.js +0 -219
- package/src/role.js +0 -110
- package/src/team.js +0 -130
- package/src/transports.js +0 -357
- package/src/types.js +0 -71
|
@@ -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
|
+
})
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for generate() - Content generation primitive
|
|
3
|
+
*
|
|
4
|
+
* The generate() function provides content generation with rich metadata
|
|
5
|
+
* about the generation process. Unlike ai-functions.generate() which is a
|
|
6
|
+
* lower-level type-dispatch function, this function returns GenerateResult
|
|
7
|
+
* with content, metadata (model, tokens, duration), and content type info.
|
|
8
|
+
*
|
|
9
|
+
* These tests use real AI calls via the Cloudflare AI Gateway.
|
|
10
|
+
* Tests are skipped if AI_GATEWAY_URL is not configured.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'vitest'
|
|
14
|
+
import { generate } from '../src/index.js'
|
|
15
|
+
|
|
16
|
+
// Skip tests if no gateway configured
|
|
17
|
+
const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
|
|
18
|
+
|
|
19
|
+
describe('generate() - Content Generation Primitive', () => {
|
|
20
|
+
describe('Unit Tests (no AI)', () => {
|
|
21
|
+
it('should be exported from index', () => {
|
|
22
|
+
expect(generate).toBeDefined()
|
|
23
|
+
expect(typeof generate).toBe('function')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should have variations method', () => {
|
|
27
|
+
expect(generate.variations).toBeDefined()
|
|
28
|
+
expect(typeof generate.variations).toBe('function')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should have withTone method', () => {
|
|
32
|
+
expect(generate.withTone).toBeDefined()
|
|
33
|
+
expect(typeof generate.withTone).toBe('function')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should have forAudience method', () => {
|
|
37
|
+
expect(generate.forAudience).toBeDefined()
|
|
38
|
+
expect(typeof generate.forAudience).toBe('function')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('should have withLength method', () => {
|
|
42
|
+
expect(generate.withLength).toBeDefined()
|
|
43
|
+
expect(typeof generate.withLength).toBe('function')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should have refine method', () => {
|
|
47
|
+
expect(generate.refine).toBeDefined()
|
|
48
|
+
expect(typeof generate.refine).toBe('function')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - Text Generation', () => {
|
|
53
|
+
it('should generate text content', async () => {
|
|
54
|
+
const result = await generate('Write a haiku about coding')
|
|
55
|
+
|
|
56
|
+
expect(result).toBeDefined()
|
|
57
|
+
expect(result.content).toBeDefined()
|
|
58
|
+
expect(typeof result.content).toBe('string')
|
|
59
|
+
expect(result.type).toBe('text')
|
|
60
|
+
expect(result.metadata).toBeDefined()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should include metadata with duration', async () => {
|
|
64
|
+
const result = await generate('Say hello in one word')
|
|
65
|
+
|
|
66
|
+
expect(result.metadata).toBeDefined()
|
|
67
|
+
expect(result.metadata?.duration).toBeDefined()
|
|
68
|
+
expect(typeof result.metadata?.duration).toBe('number')
|
|
69
|
+
expect(result.metadata?.duration).toBeGreaterThan(0)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should include model in metadata', async () => {
|
|
73
|
+
const result = await generate('Generate a greeting', {
|
|
74
|
+
model: 'sonnet',
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
expect(result.metadata?.model).toBeDefined()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should respect instructions', async () => {
|
|
81
|
+
const result = await generate('Write a product description', {
|
|
82
|
+
type: 'text',
|
|
83
|
+
instructions: 'Keep it under 20 words. Be enthusiastic.',
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(result.content).toBeDefined()
|
|
87
|
+
expect(result.type).toBe('text')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - Structured Generation', () => {
|
|
92
|
+
it('should generate structured content with schema', async () => {
|
|
93
|
+
const result = await generate<{ name: string; description: string }>('Create a product', {
|
|
94
|
+
type: 'structured',
|
|
95
|
+
schema: {
|
|
96
|
+
name: 'Product name',
|
|
97
|
+
description: 'Short product description',
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
expect(result).toBeDefined()
|
|
102
|
+
expect(result.content).toBeDefined()
|
|
103
|
+
expect(result.type).toBe('structured')
|
|
104
|
+
expect(typeof result.content).toBe('object')
|
|
105
|
+
expect((result.content as { name: string }).name).toBeDefined()
|
|
106
|
+
expect((result.content as { description: string }).description).toBeDefined()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should generate complex structured content', async () => {
|
|
110
|
+
const result = await generate<{
|
|
111
|
+
title: string
|
|
112
|
+
sections: string[]
|
|
113
|
+
author: { name: string; expertise: string }
|
|
114
|
+
}>('Create a technical article outline', {
|
|
115
|
+
type: 'structured',
|
|
116
|
+
schema: {
|
|
117
|
+
title: 'Article title',
|
|
118
|
+
sections: ['List of section headings'],
|
|
119
|
+
author: {
|
|
120
|
+
name: 'Author name',
|
|
121
|
+
expertise: 'Area of expertise',
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
expect(result.content).toBeDefined()
|
|
127
|
+
expect((result.content as { title: string }).title).toBeDefined()
|
|
128
|
+
expect(Array.isArray((result.content as { sections: string[] }).sections)).toBe(true)
|
|
129
|
+
expect((result.content as { author: { name: string } }).author.name).toBeDefined()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should throw error for structured without schema', async () => {
|
|
133
|
+
await expect(generate('Generate something', { type: 'structured' })).rejects.toThrow(
|
|
134
|
+
'Schema is required'
|
|
135
|
+
)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - Code Generation', () => {
|
|
140
|
+
it('should generate code', async () => {
|
|
141
|
+
const result = await generate('Write a function to add two numbers', {
|
|
142
|
+
type: 'code',
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
expect(result).toBeDefined()
|
|
146
|
+
expect(result.content).toBeDefined()
|
|
147
|
+
expect(result.type).toBe('code')
|
|
148
|
+
expect(result.metadata?.language).toBeDefined()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should include explanation for code', async () => {
|
|
152
|
+
const result = await generate('Write a TypeScript function to reverse a string', {
|
|
153
|
+
type: 'code',
|
|
154
|
+
instructions: 'Use modern ES6+ syntax',
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
expect(result.metadata?.explanation).toBeDefined()
|
|
158
|
+
expect(typeof result.metadata?.explanation).toBe('string')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - Variations', () => {
|
|
163
|
+
it('should generate multiple variations', async () => {
|
|
164
|
+
const variations = await generate.variations('Write a catchy tagline for a coffee shop', 3, {
|
|
165
|
+
type: 'text',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
expect(variations).toBeDefined()
|
|
169
|
+
expect(Array.isArray(variations)).toBe(true)
|
|
170
|
+
expect(variations.length).toBe(3)
|
|
171
|
+
|
|
172
|
+
variations.forEach((v) => {
|
|
173
|
+
expect(v.content).toBeDefined()
|
|
174
|
+
expect(v.type).toBe('text')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('should generate unique variations', async () => {
|
|
179
|
+
const variations = await generate.variations('Generate a random color name', 2)
|
|
180
|
+
|
|
181
|
+
expect(variations.length).toBe(2)
|
|
182
|
+
// Variations should be defined (may or may not be unique)
|
|
183
|
+
variations.forEach((v) => {
|
|
184
|
+
expect(v.content).toBeDefined()
|
|
185
|
+
})
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - Tone', () => {
|
|
190
|
+
it('should generate with professional tone', async () => {
|
|
191
|
+
const result = await generate.withTone(
|
|
192
|
+
'Write an email declining a meeting invitation',
|
|
193
|
+
'professional'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
expect(result.content).toBeDefined()
|
|
197
|
+
expect(typeof result.content).toBe('string')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should generate with friendly tone', async () => {
|
|
201
|
+
const result = await generate.withTone('Write a thank you message', 'friendly')
|
|
202
|
+
|
|
203
|
+
expect(result.content).toBeDefined()
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should generate with formal tone', async () => {
|
|
207
|
+
const result = await generate.withTone('Write a business letter introduction', 'formal')
|
|
208
|
+
|
|
209
|
+
expect(result.content).toBeDefined()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should support all tone options', async () => {
|
|
213
|
+
const tones = [
|
|
214
|
+
'professional',
|
|
215
|
+
'casual',
|
|
216
|
+
'friendly',
|
|
217
|
+
'formal',
|
|
218
|
+
'humorous',
|
|
219
|
+
'empathetic',
|
|
220
|
+
] as const
|
|
221
|
+
|
|
222
|
+
for (const tone of tones.slice(0, 2)) {
|
|
223
|
+
// Test just a couple to save time
|
|
224
|
+
const result = await generate.withTone('Write a greeting', tone)
|
|
225
|
+
expect(result.content).toBeDefined()
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - Audience', () => {
|
|
231
|
+
it('should generate for technical audience', async () => {
|
|
232
|
+
const result = await generate.forAudience('Explain how HTTP works', 'software engineers')
|
|
233
|
+
|
|
234
|
+
expect(result.content).toBeDefined()
|
|
235
|
+
expect(typeof result.content).toBe('string')
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
it('should generate for non-technical audience', async () => {
|
|
239
|
+
const result = await generate.forAudience(
|
|
240
|
+
'Explain how HTTP works',
|
|
241
|
+
'non-technical business stakeholders'
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
expect(result.content).toBeDefined()
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - Length', () => {
|
|
249
|
+
it('should generate brief content', async () => {
|
|
250
|
+
const result = await generate.withLength('Describe a sunset', 'brief')
|
|
251
|
+
|
|
252
|
+
expect(result.content).toBeDefined()
|
|
253
|
+
// Brief should be short
|
|
254
|
+
expect((result.content as string).length).toBeLessThan(200)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('should generate short content', async () => {
|
|
258
|
+
const result = await generate.withLength('Describe a forest', 'short')
|
|
259
|
+
|
|
260
|
+
expect(result.content).toBeDefined()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should generate medium content', async () => {
|
|
264
|
+
const result = await generate.withLength('Describe a city', 'medium')
|
|
265
|
+
|
|
266
|
+
expect(result.content).toBeDefined()
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it('should support all length options', async () => {
|
|
270
|
+
const lengths = ['brief', 'short', 'medium', 'long', 'detailed'] as const
|
|
271
|
+
|
|
272
|
+
// Just test brief to save time
|
|
273
|
+
const result = await generate.withLength('Write about trees', 'brief')
|
|
274
|
+
expect(result.content).toBeDefined()
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
describe.skipIf(!hasGateway)('Integration Tests (with AI) - Refinement', () => {
|
|
279
|
+
it('should refine content iteratively', async () => {
|
|
280
|
+
const result = await generate.refine('Write a product tagline', [
|
|
281
|
+
'Make it more memorable',
|
|
282
|
+
'Add urgency',
|
|
283
|
+
])
|
|
284
|
+
|
|
285
|
+
expect(result).toBeDefined()
|
|
286
|
+
expect(result.content).toBeDefined()
|
|
287
|
+
// Refined content should exist
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should apply multiple refinements', async () => {
|
|
291
|
+
const result = await generate.refine('Write a headline', [
|
|
292
|
+
'Make it shorter',
|
|
293
|
+
'Add a call to action',
|
|
294
|
+
])
|
|
295
|
+
|
|
296
|
+
expect(result.content).toBeDefined()
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
describe('Error Handling', () => {
|
|
301
|
+
it('should throw for unsupported content type', async () => {
|
|
302
|
+
await expect(generate('Generate an image', { type: 'image' })).rejects.toThrow(
|
|
303
|
+
'not yet implemented'
|
|
304
|
+
)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it('should throw for video type', async () => {
|
|
308
|
+
await expect(generate('Generate a video', { type: 'video' })).rejects.toThrow(
|
|
309
|
+
'not yet implemented'
|
|
310
|
+
)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
it('should throw for audio type', async () => {
|
|
314
|
+
await expect(generate('Generate audio', { type: 'audio' })).rejects.toThrow(
|
|
315
|
+
'not yet implemented'
|
|
316
|
+
)
|
|
317
|
+
})
|
|
318
|
+
})
|
|
319
|
+
})
|