digital-workers 2.1.3 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +2 -0
  4. package/dist/actions.d.ts.map +1 -1
  5. package/dist/actions.js +33 -21
  6. package/dist/actions.js.map +1 -1
  7. package/dist/agent-comms.d.ts.map +1 -1
  8. package/dist/agent-comms.js +36 -25
  9. package/dist/agent-comms.js.map +1 -1
  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.js +3 -3
  23. package/dist/capability-tiers.js.map +1 -1
  24. package/dist/cascade-context.d.ts +28 -28
  25. package/dist/client.d.ts +162 -0
  26. package/dist/client.d.ts.map +1 -0
  27. package/dist/client.js +64 -0
  28. package/dist/client.js.map +1 -0
  29. package/dist/decide.d.ts +42 -6
  30. package/dist/decide.d.ts.map +1 -1
  31. package/dist/decide.js +54 -11
  32. package/dist/decide.js.map +1 -1
  33. package/dist/do.d.ts +36 -7
  34. package/dist/do.d.ts.map +1 -1
  35. package/dist/do.js +82 -39
  36. package/dist/do.js.map +1 -1
  37. package/dist/error-escalation.d.ts.map +1 -1
  38. package/dist/error-escalation.js +38 -38
  39. package/dist/error-escalation.js.map +1 -1
  40. package/dist/generate.d.ts +48 -7
  41. package/dist/generate.d.ts.map +1 -1
  42. package/dist/generate.js +49 -8
  43. package/dist/generate.js.map +1 -1
  44. package/dist/goals.d.ts +10 -9
  45. package/dist/goals.d.ts.map +1 -1
  46. package/dist/goals.js +30 -24
  47. package/dist/goals.js.map +1 -1
  48. package/dist/image.d.ts +189 -0
  49. package/dist/image.d.ts.map +1 -0
  50. package/dist/image.js +528 -0
  51. package/dist/image.js.map +1 -0
  52. package/dist/index.d.ts +49 -2
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +58 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/is.d.ts +45 -10
  57. package/dist/is.d.ts.map +1 -1
  58. package/dist/is.js +56 -21
  59. package/dist/is.js.map +1 -1
  60. package/dist/kpis.d.ts +24 -15
  61. package/dist/kpis.d.ts.map +1 -1
  62. package/dist/kpis.js +16 -14
  63. package/dist/kpis.js.map +1 -1
  64. package/dist/load-balancing.d.ts.map +1 -1
  65. package/dist/load-balancing.js +124 -38
  66. package/dist/load-balancing.js.map +1 -1
  67. package/dist/logger.d.ts +76 -0
  68. package/dist/logger.d.ts.map +1 -0
  69. package/dist/logger.js +39 -0
  70. package/dist/logger.js.map +1 -0
  71. package/dist/notify.d.ts +38 -9
  72. package/dist/notify.d.ts.map +1 -1
  73. package/dist/notify.js +72 -17
  74. package/dist/notify.js.map +1 -1
  75. package/dist/role.d.ts +5 -4
  76. package/dist/role.d.ts.map +1 -1
  77. package/dist/role.js +13 -10
  78. package/dist/role.js.map +1 -1
  79. package/dist/runtime.d.ts +310 -0
  80. package/dist/runtime.d.ts.map +1 -0
  81. package/dist/runtime.js +510 -0
  82. package/dist/runtime.js.map +1 -0
  83. package/dist/team.d.ts +11 -6
  84. package/dist/team.d.ts.map +1 -1
  85. package/dist/team.js +22 -15
  86. package/dist/team.js.map +1 -1
  87. package/dist/transports/email.d.ts +318 -0
  88. package/dist/transports/email.d.ts.map +1 -0
  89. package/dist/transports/email.js +779 -0
  90. package/dist/transports/email.js.map +1 -0
  91. package/dist/transports/slack.d.ts +515 -0
  92. package/dist/transports/slack.d.ts.map +1 -0
  93. package/dist/transports/slack.js +844 -0
  94. package/dist/transports/slack.js.map +1 -0
  95. package/dist/transports.d.ts.map +1 -1
  96. package/dist/transports.js +44 -25
  97. package/dist/transports.js.map +1 -1
  98. package/dist/types.d.ts +141 -19
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/types.js +5 -0
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/id.d.ts +19 -0
  103. package/dist/utils/id.d.ts.map +1 -0
  104. package/dist/utils/id.js +21 -0
  105. package/dist/utils/id.js.map +1 -0
  106. package/dist/video.d.ts +203 -0
  107. package/dist/video.d.ts.map +1 -0
  108. package/dist/video.js +528 -0
  109. package/dist/video.js.map +1 -0
  110. package/dist/worker.d.ts +343 -0
  111. package/dist/worker.d.ts.map +1 -0
  112. package/dist/worker.js +698 -0
  113. package/dist/worker.js.map +1 -0
  114. package/package.json +32 -14
  115. package/src/actions.ts +39 -30
  116. package/src/agent-comms.ts +54 -92
  117. package/src/approve.ts +91 -20
  118. package/src/ask.ts +99 -25
  119. package/src/browse.ts +627 -0
  120. package/src/capability-tiers.ts +5 -5
  121. package/src/client.ts +221 -0
  122. package/src/decide.ts +81 -35
  123. package/src/do.ts +98 -52
  124. package/src/error-escalation.ts +55 -67
  125. package/src/generate.ts +52 -18
  126. package/src/goals.ts +36 -27
  127. package/src/image.ts +816 -0
  128. package/src/index.ts +187 -2
  129. package/src/is.ts +59 -25
  130. package/src/kpis.ts +41 -36
  131. package/src/load-balancing.ts +132 -46
  132. package/src/logger.ts +93 -0
  133. package/src/notify.ts +78 -17
  134. package/src/role.ts +30 -20
  135. package/src/runtime.ts +796 -0
  136. package/src/team.ts +24 -19
  137. package/src/transports/email.ts +1160 -0
  138. package/src/transports/slack.ts +1320 -0
  139. package/src/transports.ts +58 -43
  140. package/src/types.ts +174 -46
  141. package/src/utils/id.ts +21 -0
  142. package/src/video.ts +906 -0
  143. package/src/worker.ts +1007 -0
  144. package/test/approve.test.ts +305 -0
  145. package/test/ask.test.ts +274 -0
  146. package/test/browse.test.ts +361 -0
  147. package/test/decide.test.ts +252 -0
  148. package/test/do.test.ts +144 -0
  149. package/test/error-logging.test.ts +357 -0
  150. package/test/generate.test.ts +319 -0
  151. package/test/image.test.ts +398 -0
  152. package/test/is.test.ts +287 -0
  153. package/test/load-balancing-safety.test.ts +404 -0
  154. package/test/notify.test.ts +434 -0
  155. package/test/primitives.test.ts +320 -0
  156. package/test/runtime-integration.test.ts +892 -0
  157. package/test/transports/crypto.test.ts +230 -0
  158. package/test/transports/email.test.ts +866 -0
  159. package/test/transports/id-generation.test.ts +91 -0
  160. package/test/transports/slack.test.ts +760 -0
  161. package/test/type-safety.test.ts +834 -0
  162. package/test/types.test.ts +60 -2
  163. package/test/video.test.ts +530 -0
  164. package/test/worker.test.ts +1433 -0
  165. package/tsconfig.json +4 -1
  166. package/vitest.config.ts +42 -0
  167. package/wrangler.jsonc +36 -0
  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,361 @@
1
+ /**
2
+ * Tests for browse() - Browser automation primitive
3
+ *
4
+ * The browse() function routes browser automation tasks to appropriate Workers
5
+ * (AI Agents or Humans) based on capability matching. It uses AI to plan and
6
+ * execute browser actions with human fallback support.
7
+ *
8
+ * These tests verify the structure and exports of the browse module.
9
+ * Integration tests with real browser automation are skipped unless
10
+ * a browser automation environment is configured.
11
+ */
12
+
13
+ import { describe, it, expect } from 'vitest'
14
+ import { browse } from '../src/index.js'
15
+ import type {
16
+ BrowseOptions,
17
+ BrowseResult,
18
+ BrowseAction,
19
+ Viewport,
20
+ ClickOptions,
21
+ TypeOptions,
22
+ ScrollOptions,
23
+ ScreenshotOptions,
24
+ ExtractOptions,
25
+ } from '../src/browse.js'
26
+
27
+ describe('browse() - Browser Automation Primitive', () => {
28
+ describe('Structure Tests', () => {
29
+ it('should be exported from index', () => {
30
+ expect(browse).toBeDefined()
31
+ expect(typeof browse).toBe('function')
32
+ })
33
+
34
+ it('should have click method', () => {
35
+ expect(browse.click).toBeDefined()
36
+ expect(typeof browse.click).toBe('function')
37
+ })
38
+
39
+ it('should have type method', () => {
40
+ expect(browse.type).toBeDefined()
41
+ expect(typeof browse.type).toBe('function')
42
+ })
43
+
44
+ it('should have scroll method', () => {
45
+ expect(browse.scroll).toBeDefined()
46
+ expect(typeof browse.scroll).toBe('function')
47
+ })
48
+
49
+ it('should have screenshot method', () => {
50
+ expect(browse.screenshot).toBeDefined()
51
+ expect(typeof browse.screenshot).toBe('function')
52
+ })
53
+
54
+ it('should have extract method', () => {
55
+ expect(browse.extract).toBeDefined()
56
+ expect(typeof browse.extract).toBe('function')
57
+ })
58
+
59
+ it('should have waitFor method', () => {
60
+ expect(browse.waitFor).toBeDefined()
61
+ expect(typeof browse.waitFor).toBe('function')
62
+ })
63
+
64
+ it('should have fill method', () => {
65
+ expect(browse.fill).toBeDefined()
66
+ expect(typeof browse.fill).toBe('function')
67
+ })
68
+
69
+ it('should have crawl method', () => {
70
+ expect(browse.crawl).toBeDefined()
71
+ expect(typeof browse.crawl).toBe('function')
72
+ })
73
+ })
74
+
75
+ describe('Type Tests', () => {
76
+ it('should accept valid BrowseOptions', () => {
77
+ const options: BrowseOptions = {
78
+ url: 'https://example.com',
79
+ task: 'Extract page title',
80
+ timeout: 30000,
81
+ headless: true,
82
+ viewport: { width: 1280, height: 720 },
83
+ context: { key: 'value' },
84
+ waitFor: '.content',
85
+ humanFallback: false,
86
+ }
87
+
88
+ expect(options.url).toBe('https://example.com')
89
+ expect(options.task).toBe('Extract page title')
90
+ expect(options.timeout).toBe(30000)
91
+ expect(options.headless).toBe(true)
92
+ expect(options.viewport?.width).toBe(1280)
93
+ expect(options.viewport?.height).toBe(720)
94
+ })
95
+
96
+ it('should accept valid BrowseResult', () => {
97
+ const result: BrowseResult = {
98
+ success: true,
99
+ data: { title: 'Example' },
100
+ screenshot: 'base64string',
101
+ actions: [
102
+ { type: 'navigate', target: 'https://example.com', success: true },
103
+ { type: 'click', target: '#button', success: true },
104
+ ],
105
+ duration: 1500,
106
+ finalUrl: 'https://example.com/page',
107
+ title: 'Example Page',
108
+ }
109
+
110
+ expect(result.success).toBe(true)
111
+ expect(result.data).toEqual({ title: 'Example' })
112
+ expect(result.actions.length).toBe(2)
113
+ expect(result.duration).toBe(1500)
114
+ })
115
+
116
+ it('should accept valid BrowseAction types', () => {
117
+ const actions: BrowseAction[] = [
118
+ { type: 'navigate', target: 'https://example.com' },
119
+ { type: 'click', target: '#submit' },
120
+ { type: 'type', target: '#input', value: 'hello' },
121
+ { type: 'scroll', value: '500' },
122
+ { type: 'wait', value: '1000' },
123
+ { type: 'screenshot' },
124
+ { type: 'extract' },
125
+ ]
126
+
127
+ expect(actions.length).toBe(7)
128
+ expect(actions[0].type).toBe('navigate')
129
+ expect(actions[2].value).toBe('hello')
130
+ })
131
+
132
+ it('should accept valid Viewport', () => {
133
+ const viewport: Viewport = {
134
+ width: 1920,
135
+ height: 1080,
136
+ }
137
+
138
+ expect(viewport.width).toBe(1920)
139
+ expect(viewport.height).toBe(1080)
140
+ })
141
+
142
+ it('should accept valid ClickOptions', () => {
143
+ const options: ClickOptions = {
144
+ waitForNavigation: true,
145
+ offset: { x: 10, y: 10 },
146
+ clickCount: 2,
147
+ button: 'left',
148
+ }
149
+
150
+ expect(options.waitForNavigation).toBe(true)
151
+ expect(options.clickCount).toBe(2)
152
+ expect(options.button).toBe('left')
153
+ })
154
+
155
+ it('should accept valid TypeOptions', () => {
156
+ const options: TypeOptions = {
157
+ clear: true,
158
+ delay: 50,
159
+ pressEnter: true,
160
+ }
161
+
162
+ expect(options.clear).toBe(true)
163
+ expect(options.delay).toBe(50)
164
+ expect(options.pressEnter).toBe(true)
165
+ })
166
+
167
+ it('should accept valid ScrollOptions', () => {
168
+ const options: ScrollOptions = {
169
+ direction: 'down',
170
+ amount: 500,
171
+ toElement: '#target',
172
+ smooth: true,
173
+ }
174
+
175
+ expect(options.direction).toBe('down')
176
+ expect(options.amount).toBe(500)
177
+ expect(options.toElement).toBe('#target')
178
+ expect(options.smooth).toBe(true)
179
+ })
180
+
181
+ it('should accept valid ScreenshotOptions', () => {
182
+ const options: ScreenshotOptions = {
183
+ fullPage: true,
184
+ selector: '#content',
185
+ format: 'png',
186
+ quality: 80,
187
+ }
188
+
189
+ expect(options.fullPage).toBe(true)
190
+ expect(options.selector).toBe('#content')
191
+ expect(options.format).toBe('png')
192
+ expect(options.quality).toBe(80)
193
+ })
194
+
195
+ it('should accept valid ExtractOptions', () => {
196
+ const options: ExtractOptions = {
197
+ schema: { title: 'Page title' },
198
+ selector: 'article',
199
+ multiple: true,
200
+ includeHtml: false,
201
+ }
202
+
203
+ expect(options.schema).toEqual({ title: 'Page title' })
204
+ expect(options.selector).toBe('article')
205
+ expect(options.multiple).toBe(true)
206
+ expect(options.includeHtml).toBe(false)
207
+ })
208
+ })
209
+
210
+ describe('Unit Tests (no browser)', () => {
211
+ it('should return function signature for browse', () => {
212
+ // browse(url, task, options?) => Promise<BrowseResult>
213
+ expect(browse.length).toBeGreaterThanOrEqual(2)
214
+ })
215
+
216
+ it('should return function signature for browse.click', () => {
217
+ // browse.click(url, selector, options?) => Promise<BrowseResult>
218
+ expect(browse.click.length).toBeGreaterThanOrEqual(2)
219
+ })
220
+
221
+ it('should return function signature for browse.type', () => {
222
+ // browse.type(url, selector, text, options?) => Promise<BrowseResult>
223
+ expect(browse.type.length).toBeGreaterThanOrEqual(3)
224
+ })
225
+
226
+ it('should return function signature for browse.scroll', () => {
227
+ // browse.scroll(url, direction, amount?, options?) => Promise<BrowseResult>
228
+ expect(browse.scroll.length).toBeGreaterThanOrEqual(2)
229
+ })
230
+
231
+ it('should return function signature for browse.screenshot', () => {
232
+ // browse.screenshot(url, options?) => Promise<BrowseResult>
233
+ expect(browse.screenshot.length).toBeGreaterThanOrEqual(1)
234
+ })
235
+
236
+ it('should return function signature for browse.extract', () => {
237
+ // browse.extract(url, schema, options?) => Promise<BrowseResult>
238
+ expect(browse.extract.length).toBeGreaterThanOrEqual(2)
239
+ })
240
+
241
+ it('should return function signature for browse.waitFor', () => {
242
+ // browse.waitFor(url, selector, timeout?) => Promise<BrowseResult>
243
+ expect(browse.waitFor.length).toBeGreaterThanOrEqual(2)
244
+ })
245
+
246
+ it('should return function signature for browse.fill', () => {
247
+ // browse.fill(url, formData, submitSelector?) => Promise<BrowseResult>
248
+ expect(browse.fill.length).toBeGreaterThanOrEqual(2)
249
+ })
250
+
251
+ it('should return function signature for browse.crawl', () => {
252
+ // browse.crawl(urls, taskPerPage) => Promise<BrowseResult[]>
253
+ expect(browse.crawl.length).toBeGreaterThanOrEqual(2)
254
+ })
255
+ })
256
+
257
+ // Skip integration tests if no AI gateway configured
258
+ const hasGateway = !!process.env.AI_GATEWAY_URL || !!process.env.ANTHROPIC_API_KEY
259
+
260
+ describe.skipIf(!hasGateway)('Integration Tests (with AI)', () => {
261
+ it('should execute a simple browse task', async () => {
262
+ const result = await browse('https://example.com', 'Get the page title', {
263
+ timeout: 30000,
264
+ })
265
+
266
+ expect(result).toBeDefined()
267
+ expect(typeof result.success).toBe('boolean')
268
+ expect(Array.isArray(result.actions)).toBe(true)
269
+ expect(result.actions.length).toBeGreaterThan(0)
270
+ expect(result.actions[0].type).toBe('navigate')
271
+ })
272
+
273
+ it('should include duration in result', async () => {
274
+ const result = await browse('https://example.com', 'Check if page loaded', {
275
+ timeout: 30000,
276
+ })
277
+
278
+ expect(result.duration).toBeDefined()
279
+ expect(typeof result.duration).toBe('number')
280
+ expect(result.duration).toBeGreaterThan(0)
281
+ })
282
+
283
+ it('should handle timeout option', async () => {
284
+ const result = await browse('https://example.com', 'Perform complex task', {
285
+ timeout: 1, // Very short timeout
286
+ })
287
+
288
+ // Should fail or succeed quickly
289
+ expect(result).toBeDefined()
290
+ expect(typeof result.success).toBe('boolean')
291
+ if (!result.success) {
292
+ expect(result.error).toBeDefined()
293
+ }
294
+ })
295
+
296
+ it('should support human fallback option', async () => {
297
+ const result = await browse('https://example.com', 'Complete complex form', {
298
+ humanFallback: true,
299
+ timeout: 1, // Force failure
300
+ })
301
+
302
+ expect(result).toBeDefined()
303
+ // When AI fails with humanFallback, executedBy should indicate pending
304
+ if (!result.success && result.executedBy) {
305
+ expect(result.executedBy).toBe('pending-human-fallback')
306
+ }
307
+ })
308
+
309
+ it('should execute click helper', async () => {
310
+ const result = await browse.click('https://example.com', 'a')
311
+
312
+ expect(result).toBeDefined()
313
+ expect(typeof result.success).toBe('boolean')
314
+ expect(Array.isArray(result.actions)).toBe(true)
315
+ })
316
+
317
+ it('should execute type helper', async () => {
318
+ const result = await browse.type('https://example.com', 'input', 'test text')
319
+
320
+ expect(result).toBeDefined()
321
+ expect(typeof result.success).toBe('boolean')
322
+ })
323
+
324
+ it('should execute scroll helper', async () => {
325
+ const result = await browse.scroll('https://example.com', 'down', 100)
326
+
327
+ expect(result).toBeDefined()
328
+ expect(typeof result.success).toBe('boolean')
329
+ })
330
+
331
+ it('should execute screenshot helper', async () => {
332
+ const result = await browse.screenshot('https://example.com')
333
+
334
+ expect(result).toBeDefined()
335
+ expect(typeof result.success).toBe('boolean')
336
+ })
337
+
338
+ it('should execute extract helper', async () => {
339
+ const result = await browse.extract('https://example.com', {
340
+ title: 'Page title',
341
+ })
342
+
343
+ expect(result).toBeDefined()
344
+ expect(typeof result.success).toBe('boolean')
345
+ })
346
+
347
+ it('should execute crawl for multiple URLs', async () => {
348
+ const results = await browse.crawl(
349
+ ['https://example.com', 'https://example.org'],
350
+ 'Get page title'
351
+ )
352
+
353
+ expect(results).toBeDefined()
354
+ expect(Array.isArray(results)).toBe(true)
355
+ expect(results.length).toBe(2)
356
+ results.forEach((result) => {
357
+ expect(typeof result.success).toBe('boolean')
358
+ })
359
+ })
360
+ })
361
+ })
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Tests for decide() - Decision making primitive
3
+ *
4
+ * The decide() function provides structured decision-making with criteria
5
+ * evaluation, confidence scoring, and optional human approval. Unlike
6
+ * ai-functions.decide() which is an LLM-as-judge comparison, this function
7
+ * provides comprehensive decision analysis.
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 { decide } 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('decide() - Decision Making Primitive', () => {
20
+ describe('Unit Tests (no AI)', () => {
21
+ it('should be exported from index', () => {
22
+ expect(decide).toBeDefined()
23
+ expect(typeof decide).toBe('function')
24
+ })
25
+
26
+ it('should have yesNo method', () => {
27
+ expect(decide.yesNo).toBeDefined()
28
+ expect(typeof decide.yesNo).toBe('function')
29
+ })
30
+
31
+ it('should have prioritize method', () => {
32
+ expect(decide.prioritize).toBeDefined()
33
+ expect(typeof decide.prioritize).toBe('function')
34
+ })
35
+
36
+ it('should have withApproval method', () => {
37
+ expect(decide.withApproval).toBeDefined()
38
+ expect(typeof decide.withApproval).toBe('function')
39
+ })
40
+ })
41
+
42
+ describe.skipIf(!hasGateway)('Integration Tests (with AI) - Basic Decisions', () => {
43
+ it('should make a decision from options', async () => {
44
+ const decision = await decide({
45
+ options: ['Option A', 'Option B', 'Option C'],
46
+ context: 'Choose the best option for a simple test',
47
+ })
48
+
49
+ expect(decision).toBeDefined()
50
+ expect(decision.choice).toBeDefined()
51
+ expect(['Option A', 'Option B', 'Option C']).toContain(decision.choice)
52
+ expect(decision.reasoning).toBeDefined()
53
+ expect(typeof decision.reasoning).toBe('string')
54
+ expect(decision.confidence).toBeDefined()
55
+ expect(typeof decision.confidence).toBe('number')
56
+ expect(decision.confidence).toBeGreaterThanOrEqual(0)
57
+ expect(decision.confidence).toBeLessThanOrEqual(1)
58
+ })
59
+
60
+ it('should include alternatives in decision', async () => {
61
+ const decision = await decide({
62
+ options: ['React', 'Vue', 'Svelte'],
63
+ context: 'Choose a frontend framework for a small project',
64
+ })
65
+
66
+ expect(decision).toBeDefined()
67
+ expect(decision.alternatives).toBeDefined()
68
+ expect(Array.isArray(decision.alternatives)).toBe(true)
69
+ })
70
+
71
+ it('should support criteria evaluation', async () => {
72
+ const decision = await decide({
73
+ options: ['PostgreSQL', 'MongoDB', 'Redis'],
74
+ context: 'Choose a database for an e-commerce application',
75
+ criteria: ['ACID compliance', 'Scalability', 'Ease of use', 'Community support'],
76
+ })
77
+
78
+ expect(decision).toBeDefined()
79
+ expect(decision.choice).toBeDefined()
80
+ expect(decision.reasoning).toBeDefined()
81
+ // Reasoning should mention criteria
82
+ expect(decision.reasoning.length).toBeGreaterThan(10)
83
+ })
84
+
85
+ it('should handle object options', async () => {
86
+ const decision = await decide({
87
+ options: [
88
+ { id: 'plan-a', name: 'Basic Plan', price: 10 },
89
+ { id: 'plan-b', name: 'Pro Plan', price: 25 },
90
+ { id: 'plan-c', name: 'Enterprise Plan', price: 100 },
91
+ ],
92
+ context: 'Choose the best plan for a startup with limited budget',
93
+ })
94
+
95
+ expect(decision).toBeDefined()
96
+ expect(decision.choice).toBeDefined()
97
+ })
98
+
99
+ it('should respect includeReasoning option', async () => {
100
+ const decision = await decide({
101
+ options: ['Yes', 'No'],
102
+ context: 'Should we proceed?',
103
+ includeReasoning: true,
104
+ })
105
+
106
+ expect(decision.reasoning).toBeDefined()
107
+ expect(decision.reasoning.length).toBeGreaterThan(0)
108
+ })
109
+
110
+ it('should handle complex context object', async () => {
111
+ const decision = await decide({
112
+ options: ['Approve', 'Reject', 'Request more info'],
113
+ context: {
114
+ requestType: 'Budget increase',
115
+ amount: 50000,
116
+ department: 'Engineering',
117
+ justification: 'Need to hire additional developer',
118
+ currentBudget: 200000,
119
+ utilizationRate: 0.95,
120
+ },
121
+ })
122
+
123
+ expect(decision).toBeDefined()
124
+ expect(decision.choice).toBeDefined()
125
+ })
126
+ })
127
+
128
+ describe.skipIf(!hasGateway)('Integration Tests (with AI) - Yes/No Decisions', () => {
129
+ it('should make binary decision', async () => {
130
+ const decision = await decide.yesNo(
131
+ 'Should we deploy on Friday?',
132
+ 'The deploy is a minor bug fix with good test coverage'
133
+ )
134
+
135
+ expect(decision).toBeDefined()
136
+ expect(['yes', 'no']).toContain(decision.choice)
137
+ expect(decision.reasoning).toBeDefined()
138
+ expect(decision.confidence).toBeDefined()
139
+ })
140
+
141
+ it('should handle context object in yesNo', async () => {
142
+ const decision = await decide.yesNo('Should we approve this expense?', {
143
+ amount: 500,
144
+ category: 'Software',
145
+ policy: 'Software expenses under $1000 are auto-approved',
146
+ })
147
+
148
+ expect(decision).toBeDefined()
149
+ expect(['yes', 'no']).toContain(decision.choice)
150
+ })
151
+
152
+ it('should provide confidence score for yesNo', async () => {
153
+ const decision = await decide.yesNo('Is this a valid email format: test@example.com?')
154
+
155
+ expect(decision.confidence).toBeGreaterThan(0)
156
+ })
157
+ })
158
+
159
+ describe.skipIf(!hasGateway)('Integration Tests (with AI) - Prioritization', () => {
160
+ it('should prioritize items', async () => {
161
+ const prioritized = await decide.prioritize(
162
+ ['Feature A', 'Bug Fix B', 'Tech Debt C', 'Feature D'],
163
+ 'Sprint planning for a 2-week sprint'
164
+ )
165
+
166
+ expect(prioritized).toBeDefined()
167
+ expect(Array.isArray(prioritized)).toBe(true)
168
+ expect(prioritized.length).toBe(4)
169
+
170
+ prioritized.forEach((item) => {
171
+ expect(item.choice).toBeDefined()
172
+ expect(item.rank).toBeDefined()
173
+ expect(typeof item.rank).toBe('number')
174
+ expect(item.reasoning).toBeDefined()
175
+ expect(item.confidence).toBeDefined()
176
+ })
177
+ })
178
+
179
+ it('should prioritize with criteria', async () => {
180
+ const prioritized = await decide.prioritize(
181
+ ['Task 1', 'Task 2', 'Task 3'],
182
+ 'Project backlog',
183
+ ['User impact', 'Urgency', 'Effort required']
184
+ )
185
+
186
+ expect(prioritized).toBeDefined()
187
+ expect(prioritized.length).toBe(3)
188
+
189
+ // All items should have a rank
190
+ const ranks = prioritized.map((p) => p.rank)
191
+ expect(ranks.every((r) => typeof r === 'number')).toBe(true)
192
+ })
193
+
194
+ it('should prioritize object items', async () => {
195
+ const items = [
196
+ { id: 1, name: 'Critical bug', severity: 'high' },
197
+ { id: 2, name: 'New feature', severity: 'low' },
198
+ { id: 3, name: 'Performance fix', severity: 'medium' },
199
+ ]
200
+
201
+ const prioritized = await decide.prioritize(items, 'Bug triage session')
202
+
203
+ expect(prioritized).toBeDefined()
204
+ expect(prioritized.length).toBe(3)
205
+ })
206
+ })
207
+
208
+ describe.skipIf(!hasGateway)('Integration Tests (with AI) - Decision Quality', () => {
209
+ it('should provide meaningful reasoning', async () => {
210
+ const decision = await decide({
211
+ options: ['AWS', 'GCP', 'Azure'],
212
+ context: 'Choosing a cloud provider for a machine learning startup',
213
+ criteria: ['ML services', 'Pricing', 'Documentation', 'Support'],
214
+ })
215
+
216
+ expect(decision.reasoning).toBeDefined()
217
+ expect(decision.reasoning.length).toBeGreaterThan(50)
218
+ // Reasoning should be substantive
219
+ })
220
+
221
+ it('should score alternatives', async () => {
222
+ const decision = await decide({
223
+ options: ['TypeScript', 'Python', 'Go', 'Rust'],
224
+ context: 'Choosing a language for a CLI tool',
225
+ })
226
+
227
+ if (decision.alternatives && decision.alternatives.length > 0) {
228
+ decision.alternatives.forEach((alt) => {
229
+ expect(alt.option).toBeDefined()
230
+ expect(typeof alt.score).toBe('number')
231
+ })
232
+ }
233
+ })
234
+
235
+ it('should normalize confidence to 0-1 range', async () => {
236
+ const decision = await decide({
237
+ options: ['A', 'B'],
238
+ context: 'Simple choice',
239
+ })
240
+
241
+ expect(decision.confidence).toBeGreaterThanOrEqual(0)
242
+ expect(decision.confidence).toBeLessThanOrEqual(1)
243
+ })
244
+ })
245
+
246
+ describe('Decision with Approval', () => {
247
+ it('should have withApproval method that requires approver', async () => {
248
+ // Just verify the signature - actual approval routing is tested in approve.test.ts
249
+ expect(typeof decide.withApproval).toBe('function')
250
+ })
251
+ })
252
+ })