digital-workers 2.1.3 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +2 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +33 -21
- package/dist/actions.js.map +1 -1
- package/dist/agent-comms.d.ts.map +1 -1
- package/dist/agent-comms.js +36 -25
- package/dist/agent-comms.js.map +1 -1
- 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.js +3 -3
- package/dist/capability-tiers.js.map +1 -1
- package/dist/cascade-context.d.ts +28 -28
- 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.map +1 -1
- package/dist/error-escalation.js +38 -38
- package/dist/error-escalation.js.map +1 -1
- 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 +49 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -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.map +1 -1
- package/dist/load-balancing.js +124 -38
- package/dist/load-balancing.js.map +1 -1
- 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 +141 -19
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -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 +32 -14
- package/src/actions.ts +39 -30
- package/src/agent-comms.ts +54 -92
- package/src/approve.ts +91 -20
- package/src/ask.ts +99 -25
- package/src/browse.ts +627 -0
- package/src/capability-tiers.ts +5 -5
- package/src/client.ts +221 -0
- package/src/decide.ts +81 -35
- package/src/do.ts +98 -52
- package/src/error-escalation.ts +55 -67
- package/src/generate.ts +52 -18
- package/src/goals.ts +36 -27
- package/src/image.ts +816 -0
- package/src/index.ts +187 -2
- package/src/is.ts +59 -25
- package/src/kpis.ts +41 -36
- package/src/load-balancing.ts +132 -46
- 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 +174 -46
- package/src/utils/id.ts +21 -0
- package/src/video.ts +906 -0
- package/src/worker.ts +1007 -0
- package/test/approve.test.ts +305 -0
- package/test/ask.test.ts +274 -0
- package/test/browse.test.ts +361 -0
- package/test/decide.test.ts +252 -0
- package/test/do.test.ts +144 -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/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 +60 -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 -4
- package/LICENSE +0 -21
- 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,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
|
+
})
|