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.
Files changed (197) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +136 -180
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +34 -21
  5. package/dist/actions.js.map +1 -1
  6. package/dist/agent-comms.d.ts +438 -0
  7. package/dist/agent-comms.d.ts.map +1 -0
  8. package/dist/agent-comms.js +677 -0
  9. package/dist/agent-comms.js.map +1 -0
  10. package/dist/approve.d.ts +40 -8
  11. package/dist/approve.d.ts.map +1 -1
  12. package/dist/approve.js +86 -20
  13. package/dist/approve.js.map +1 -1
  14. package/dist/ask.d.ts +38 -7
  15. package/dist/ask.d.ts.map +1 -1
  16. package/dist/ask.js +85 -25
  17. package/dist/ask.js.map +1 -1
  18. package/dist/browse.d.ts +223 -0
  19. package/dist/browse.d.ts.map +1 -0
  20. package/dist/browse.js +392 -0
  21. package/dist/browse.js.map +1 -0
  22. package/dist/capability-tiers.d.ts +230 -0
  23. package/dist/capability-tiers.d.ts.map +1 -0
  24. package/dist/capability-tiers.js +388 -0
  25. package/dist/capability-tiers.js.map +1 -0
  26. package/dist/cascade-context.d.ts +523 -0
  27. package/dist/cascade-context.d.ts.map +1 -0
  28. package/dist/cascade-context.js +494 -0
  29. package/dist/cascade-context.js.map +1 -0
  30. package/dist/client.d.ts +162 -0
  31. package/dist/client.d.ts.map +1 -0
  32. package/dist/client.js +64 -0
  33. package/dist/client.js.map +1 -0
  34. package/dist/decide.d.ts +42 -6
  35. package/dist/decide.d.ts.map +1 -1
  36. package/dist/decide.js +54 -11
  37. package/dist/decide.js.map +1 -1
  38. package/dist/do.d.ts +36 -7
  39. package/dist/do.d.ts.map +1 -1
  40. package/dist/do.js +82 -39
  41. package/dist/do.js.map +1 -1
  42. package/dist/error-escalation.d.ts +416 -0
  43. package/dist/error-escalation.d.ts.map +1 -0
  44. package/dist/error-escalation.js +656 -0
  45. package/dist/error-escalation.js.map +1 -0
  46. package/dist/generate.d.ts +48 -7
  47. package/dist/generate.d.ts.map +1 -1
  48. package/dist/generate.js +49 -8
  49. package/dist/generate.js.map +1 -1
  50. package/dist/goals.d.ts +10 -9
  51. package/dist/goals.d.ts.map +1 -1
  52. package/dist/goals.js +30 -24
  53. package/dist/goals.js.map +1 -1
  54. package/dist/image.d.ts +189 -0
  55. package/dist/image.d.ts.map +1 -0
  56. package/dist/image.js +528 -0
  57. package/dist/image.js.map +1 -0
  58. package/dist/index.d.ts +59 -2
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +92 -2
  61. package/dist/index.js.map +1 -1
  62. package/dist/is.d.ts +45 -10
  63. package/dist/is.d.ts.map +1 -1
  64. package/dist/is.js +56 -21
  65. package/dist/is.js.map +1 -1
  66. package/dist/kpis.d.ts +24 -15
  67. package/dist/kpis.d.ts.map +1 -1
  68. package/dist/kpis.js +16 -14
  69. package/dist/kpis.js.map +1 -1
  70. package/dist/load-balancing.d.ts +395 -0
  71. package/dist/load-balancing.d.ts.map +1 -0
  72. package/dist/load-balancing.js +991 -0
  73. package/dist/load-balancing.js.map +1 -0
  74. package/dist/logger.d.ts +76 -0
  75. package/dist/logger.d.ts.map +1 -0
  76. package/dist/logger.js +39 -0
  77. package/dist/logger.js.map +1 -0
  78. package/dist/notify.d.ts +38 -9
  79. package/dist/notify.d.ts.map +1 -1
  80. package/dist/notify.js +72 -17
  81. package/dist/notify.js.map +1 -1
  82. package/dist/role.d.ts +5 -4
  83. package/dist/role.d.ts.map +1 -1
  84. package/dist/role.js +13 -10
  85. package/dist/role.js.map +1 -1
  86. package/dist/runtime.d.ts +310 -0
  87. package/dist/runtime.d.ts.map +1 -0
  88. package/dist/runtime.js +510 -0
  89. package/dist/runtime.js.map +1 -0
  90. package/dist/team.d.ts +11 -6
  91. package/dist/team.d.ts.map +1 -1
  92. package/dist/team.js +22 -15
  93. package/dist/team.js.map +1 -1
  94. package/dist/transports/email.d.ts +318 -0
  95. package/dist/transports/email.d.ts.map +1 -0
  96. package/dist/transports/email.js +779 -0
  97. package/dist/transports/email.js.map +1 -0
  98. package/dist/transports/slack.d.ts +515 -0
  99. package/dist/transports/slack.d.ts.map +1 -0
  100. package/dist/transports/slack.js +844 -0
  101. package/dist/transports/slack.js.map +1 -0
  102. package/dist/transports.d.ts.map +1 -1
  103. package/dist/transports.js +44 -25
  104. package/dist/transports.js.map +1 -1
  105. package/dist/types.d.ts +149 -19
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +6 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/id.d.ts +19 -0
  110. package/dist/utils/id.d.ts.map +1 -0
  111. package/dist/utils/id.js +21 -0
  112. package/dist/utils/id.js.map +1 -0
  113. package/dist/video.d.ts +203 -0
  114. package/dist/video.d.ts.map +1 -0
  115. package/dist/video.js +528 -0
  116. package/dist/video.js.map +1 -0
  117. package/dist/worker.d.ts +343 -0
  118. package/dist/worker.d.ts.map +1 -0
  119. package/dist/worker.js +698 -0
  120. package/dist/worker.js.map +1 -0
  121. package/package.json +24 -5
  122. package/src/actions.ts +48 -38
  123. package/src/agent-comms.ts +1200 -0
  124. package/src/approve.ts +91 -20
  125. package/src/ask.ts +99 -25
  126. package/src/browse.ts +627 -0
  127. package/src/capability-tiers.ts +545 -0
  128. package/src/cascade-context.ts +648 -0
  129. package/src/client.ts +221 -0
  130. package/src/decide.ts +81 -35
  131. package/src/do.ts +98 -52
  132. package/src/error-escalation.ts +1123 -0
  133. package/src/generate.ts +52 -18
  134. package/src/goals.ts +36 -27
  135. package/src/image.ts +816 -0
  136. package/src/index.ts +410 -2
  137. package/src/is.ts +59 -25
  138. package/src/kpis.ts +41 -36
  139. package/src/load-balancing.ts +1467 -0
  140. package/src/logger.ts +93 -0
  141. package/src/notify.ts +78 -17
  142. package/src/role.ts +30 -20
  143. package/src/runtime.ts +796 -0
  144. package/src/team.ts +24 -19
  145. package/src/transports/email.ts +1160 -0
  146. package/src/transports/slack.ts +1320 -0
  147. package/src/transports.ts +58 -43
  148. package/src/types.ts +182 -46
  149. package/src/utils/id.ts +21 -0
  150. package/src/video.ts +906 -0
  151. package/src/worker.ts +1007 -0
  152. package/test/agent-comms.test.ts +1397 -0
  153. package/test/approve.test.ts +305 -0
  154. package/test/ask.test.ts +274 -0
  155. package/test/browse.test.ts +361 -0
  156. package/test/capability-tiers.test.ts +631 -0
  157. package/test/cascade-context.test.ts +692 -0
  158. package/test/decide.test.ts +252 -0
  159. package/test/do.test.ts +144 -0
  160. package/test/error-escalation.test.ts +1205 -0
  161. package/test/error-logging.test.ts +357 -0
  162. package/test/generate.test.ts +319 -0
  163. package/test/image.test.ts +398 -0
  164. package/test/is.test.ts +287 -0
  165. package/test/load-balancing-safety.test.ts +404 -0
  166. package/test/load-balancing-thread-safety.test.ts +464 -0
  167. package/test/load-balancing.test.ts +1145 -0
  168. package/test/notify.test.ts +434 -0
  169. package/test/primitives.test.ts +320 -0
  170. package/test/runtime-integration.test.ts +892 -0
  171. package/test/transports/crypto.test.ts +230 -0
  172. package/test/transports/email.test.ts +866 -0
  173. package/test/transports/id-generation.test.ts +91 -0
  174. package/test/transports/slack.test.ts +760 -0
  175. package/test/type-safety.test.ts +834 -0
  176. package/test/types.test.ts +95 -2
  177. package/test/video.test.ts +530 -0
  178. package/test/worker.test.ts +1433 -0
  179. package/tsconfig.json +4 -1
  180. package/vitest.config.ts +42 -0
  181. package/wrangler.jsonc +36 -0
  182. package/.turbo/turbo-build.log +0 -5
  183. package/src/actions.js +0 -436
  184. package/src/approve.js +0 -234
  185. package/src/ask.js +0 -226
  186. package/src/decide.js +0 -244
  187. package/src/do.js +0 -227
  188. package/src/generate.js +0 -298
  189. package/src/goals.js +0 -205
  190. package/src/index.js +0 -68
  191. package/src/is.js +0 -317
  192. package/src/kpis.js +0 -270
  193. package/src/notify.js +0 -219
  194. package/src/role.js +0 -110
  195. package/src/team.js +0 -130
  196. package/src/transports.js +0 -357
  197. 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
+ })