ai-experiments 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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +13 -1
- package/dist/clickhouse-storage.d.ts +227 -0
- package/dist/clickhouse-storage.d.ts.map +1 -0
- package/dist/clickhouse-storage.js +522 -0
- package/dist/clickhouse-storage.js.map +1 -0
- package/dist/decide.d.ts.map +1 -1
- package/dist/decide.js +6 -2
- package/dist/decide.js.map +1 -1
- package/dist/experiment.js +9 -9
- package/dist/experiment.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/tracking.js +3 -3
- package/dist/tracking.js.map +1 -1
- package/package.json +16 -16
- package/src/clickhouse-storage.ts +698 -0
- package/src/decide.ts +8 -10
- package/src/experiment.ts +12 -12
- package/src/index.ts +13 -3
- package/src/tracking.ts +3 -3
- package/test/cartesian.test.ts +287 -0
- package/test/clickhouse-storage.test.ts +470 -0
- package/test/decide.test.ts +414 -0
- package/test/experiment.test.ts +473 -0
- package/test/tracking.test.ts +347 -0
- package/vitest.config.ts +34 -0
- package/LICENSE +0 -21
- package/dist/chdb-storage.d.ts +0 -135
- package/dist/chdb-storage.d.ts.map +0 -1
- package/dist/chdb-storage.js +0 -468
- package/dist/chdb-storage.js.map +0 -1
- package/src/cartesian.js +0 -215
- package/src/chdb-storage.js +0 -464
- package/src/chdb-storage.ts +0 -567
- package/src/decide.js +0 -328
- package/src/experiment.js +0 -291
- package/src/index.js +0 -20
- package/src/tracking.js +0 -309
- package/src/types.js +0 -4
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Experiment class - A/B testing primitives
|
|
3
|
+
*
|
|
4
|
+
* These tests verify the core experiment functionality including:
|
|
5
|
+
* - Experiment creation and execution
|
|
6
|
+
* - Variant comparison
|
|
7
|
+
* - Metric calculation
|
|
8
|
+
* - Best variant selection
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
12
|
+
import {
|
|
13
|
+
Experiment,
|
|
14
|
+
createVariantsFromGrid,
|
|
15
|
+
configureTracking,
|
|
16
|
+
createMemoryBackend,
|
|
17
|
+
} from '../src/index.js'
|
|
18
|
+
|
|
19
|
+
// Use memory backend for testing to avoid console output
|
|
20
|
+
const testBackend = createMemoryBackend()
|
|
21
|
+
|
|
22
|
+
describe('Experiment', () => {
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
testBackend.clear()
|
|
25
|
+
configureTracking({ backend: testBackend, enabled: true })
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('basic experiment creation', () => {
|
|
29
|
+
it('executes variants and returns results', async () => {
|
|
30
|
+
const results = await Experiment({
|
|
31
|
+
id: 'test-experiment',
|
|
32
|
+
name: 'Test Experiment',
|
|
33
|
+
variants: [
|
|
34
|
+
{
|
|
35
|
+
id: 'variant-a',
|
|
36
|
+
name: 'Variant A',
|
|
37
|
+
config: { value: 1 },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'variant-b',
|
|
41
|
+
name: 'Variant B',
|
|
42
|
+
config: { value: 2 },
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
execute: async (config) => {
|
|
46
|
+
return { computed: config.value * 10 }
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
expect(results.experimentId).toBe('test-experiment')
|
|
51
|
+
expect(results.experimentName).toBe('Test Experiment')
|
|
52
|
+
expect(results.results).toHaveLength(2)
|
|
53
|
+
expect(results.successCount).toBe(2)
|
|
54
|
+
expect(results.failureCount).toBe(0)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('returns ExperimentSummary with all required fields', async () => {
|
|
58
|
+
const results = await Experiment({
|
|
59
|
+
id: 'summary-test',
|
|
60
|
+
name: 'Summary Test',
|
|
61
|
+
variants: [{ id: 'v1', name: 'V1', config: {} }],
|
|
62
|
+
execute: async () => 'done',
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(results).toHaveProperty('experimentId')
|
|
66
|
+
expect(results).toHaveProperty('experimentName')
|
|
67
|
+
expect(results).toHaveProperty('results')
|
|
68
|
+
expect(results).toHaveProperty('totalDuration')
|
|
69
|
+
expect(results).toHaveProperty('successCount')
|
|
70
|
+
expect(results).toHaveProperty('failureCount')
|
|
71
|
+
expect(results).toHaveProperty('startedAt')
|
|
72
|
+
expect(results).toHaveProperty('completedAt')
|
|
73
|
+
expect(results.startedAt).toBeInstanceOf(Date)
|
|
74
|
+
expect(results.completedAt).toBeInstanceOf(Date)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('handles single variant', async () => {
|
|
78
|
+
const results = await Experiment({
|
|
79
|
+
id: 'single-variant',
|
|
80
|
+
name: 'Single Variant Test',
|
|
81
|
+
variants: [{ id: 'only', name: 'Only Variant', config: { x: 42 } }],
|
|
82
|
+
execute: async (config) => config.x,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(results.results).toHaveLength(1)
|
|
86
|
+
expect(results.results[0].result).toBe(42)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('provides context to execute function', async () => {
|
|
90
|
+
let capturedContext: unknown = null
|
|
91
|
+
|
|
92
|
+
await Experiment({
|
|
93
|
+
id: 'context-test',
|
|
94
|
+
name: 'Context Test',
|
|
95
|
+
variants: [{ id: 'v1', name: 'V1', config: {} }],
|
|
96
|
+
execute: async (config, context) => {
|
|
97
|
+
capturedContext = context
|
|
98
|
+
return 'done'
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(capturedContext).not.toBeNull()
|
|
103
|
+
expect(capturedContext).toHaveProperty('experimentId', 'context-test')
|
|
104
|
+
expect(capturedContext).toHaveProperty('variantId', 'v1')
|
|
105
|
+
expect(capturedContext).toHaveProperty('runId')
|
|
106
|
+
expect(capturedContext).toHaveProperty('startedAt')
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
describe('metric calculation', () => {
|
|
111
|
+
it('computes metric for each variant', async () => {
|
|
112
|
+
const results = await Experiment({
|
|
113
|
+
id: 'metric-test',
|
|
114
|
+
name: 'Metric Test',
|
|
115
|
+
variants: [
|
|
116
|
+
{ id: 'low', name: 'Low Score', config: { score: 30 } },
|
|
117
|
+
{ id: 'high', name: 'High Score', config: { score: 90 } },
|
|
118
|
+
],
|
|
119
|
+
execute: async (config) => config.score,
|
|
120
|
+
metric: (result) => result,
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(results.results[0].metricValue).toBe(30)
|
|
124
|
+
expect(results.results[1].metricValue).toBe(90)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('identifies best variant by metric', async () => {
|
|
128
|
+
const results = await Experiment({
|
|
129
|
+
id: 'best-variant-test',
|
|
130
|
+
name: 'Best Variant Test',
|
|
131
|
+
variants: [
|
|
132
|
+
{ id: 'poor', name: 'Poor', config: { quality: 0.3 } },
|
|
133
|
+
{ id: 'good', name: 'Good', config: { quality: 0.7 } },
|
|
134
|
+
{ id: 'excellent', name: 'Excellent', config: { quality: 0.95 } },
|
|
135
|
+
],
|
|
136
|
+
execute: async (config) => ({ quality: config.quality }),
|
|
137
|
+
metric: (result) => result.quality,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
expect(results.bestVariant).toBeDefined()
|
|
141
|
+
expect(results.bestVariant?.variantId).toBe('excellent')
|
|
142
|
+
expect(results.bestVariant?.variantName).toBe('Excellent')
|
|
143
|
+
expect(results.bestVariant?.metricValue).toBe(0.95)
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('supports async metric function', async () => {
|
|
147
|
+
const results = await Experiment({
|
|
148
|
+
id: 'async-metric',
|
|
149
|
+
name: 'Async Metric Test',
|
|
150
|
+
variants: [{ id: 'v1', name: 'V1', config: { value: 42 } }],
|
|
151
|
+
execute: async (config) => config.value,
|
|
152
|
+
metric: async (result) => {
|
|
153
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
154
|
+
return result * 2
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
expect(results.results[0].metricValue).toBe(84)
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe('execution options', () => {
|
|
163
|
+
it('runs variants in parallel by default', async () => {
|
|
164
|
+
const executionOrder: string[] = []
|
|
165
|
+
|
|
166
|
+
await Experiment({
|
|
167
|
+
id: 'parallel-test',
|
|
168
|
+
name: 'Parallel Test',
|
|
169
|
+
variants: [
|
|
170
|
+
{ id: 'v1', name: 'V1', config: { delay: 50, id: 'v1' } },
|
|
171
|
+
{ id: 'v2', name: 'V2', config: { delay: 10, id: 'v2' } },
|
|
172
|
+
],
|
|
173
|
+
execute: async (config) => {
|
|
174
|
+
await new Promise((r) => setTimeout(r, config.delay))
|
|
175
|
+
executionOrder.push(config.id)
|
|
176
|
+
return config.id
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// In parallel mode, v2 should finish before v1 due to shorter delay
|
|
181
|
+
expect(executionOrder).toEqual(['v2', 'v1'])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('runs variants sequentially when parallel=false', async () => {
|
|
185
|
+
const executionOrder: string[] = []
|
|
186
|
+
|
|
187
|
+
await Experiment(
|
|
188
|
+
{
|
|
189
|
+
id: 'sequential-test',
|
|
190
|
+
name: 'Sequential Test',
|
|
191
|
+
variants: [
|
|
192
|
+
{ id: 'v1', name: 'V1', config: { delay: 20, id: 'v1' } },
|
|
193
|
+
{ id: 'v2', name: 'V2', config: { delay: 10, id: 'v2' } },
|
|
194
|
+
],
|
|
195
|
+
execute: async (config) => {
|
|
196
|
+
await new Promise((r) => setTimeout(r, config.delay))
|
|
197
|
+
executionOrder.push(config.id)
|
|
198
|
+
return config.id
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{ parallel: false }
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
// In sequential mode, v1 should finish before v2 starts
|
|
205
|
+
expect(executionOrder).toEqual(['v1', 'v2'])
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('respects maxConcurrency option', async () => {
|
|
209
|
+
let concurrent = 0
|
|
210
|
+
let maxConcurrent = 0
|
|
211
|
+
|
|
212
|
+
await Experiment(
|
|
213
|
+
{
|
|
214
|
+
id: 'concurrency-test',
|
|
215
|
+
name: 'Concurrency Test',
|
|
216
|
+
variants: Array.from({ length: 4 }, (_, i) => ({
|
|
217
|
+
id: `v${i}`,
|
|
218
|
+
name: `Variant ${i}`,
|
|
219
|
+
config: { index: i },
|
|
220
|
+
})),
|
|
221
|
+
execute: async () => {
|
|
222
|
+
concurrent++
|
|
223
|
+
maxConcurrent = Math.max(maxConcurrent, concurrent)
|
|
224
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
225
|
+
concurrent--
|
|
226
|
+
return 'done'
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
{ maxConcurrency: 2 }
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
expect(maxConcurrent).toBeLessThanOrEqual(2)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it('calls onVariantStart callback', async () => {
|
|
236
|
+
const startedVariants: string[] = []
|
|
237
|
+
|
|
238
|
+
await Experiment(
|
|
239
|
+
{
|
|
240
|
+
id: 'callback-start-test',
|
|
241
|
+
name: 'Callback Start Test',
|
|
242
|
+
variants: [
|
|
243
|
+
{ id: 'v1', name: 'V1', config: {} },
|
|
244
|
+
{ id: 'v2', name: 'V2', config: {} },
|
|
245
|
+
],
|
|
246
|
+
execute: async () => 'done',
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
parallel: false,
|
|
250
|
+
onVariantStart: (variantId) => {
|
|
251
|
+
startedVariants.push(variantId)
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
expect(startedVariants).toEqual(['v1', 'v2'])
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('calls onVariantComplete callback', async () => {
|
|
260
|
+
const completedResults: number[] = []
|
|
261
|
+
|
|
262
|
+
await Experiment(
|
|
263
|
+
{
|
|
264
|
+
id: 'callback-complete-test',
|
|
265
|
+
name: 'Callback Complete Test',
|
|
266
|
+
variants: [
|
|
267
|
+
{ id: 'v1', name: 'V1', config: { val: 10 } },
|
|
268
|
+
{ id: 'v2', name: 'V2', config: { val: 20 } },
|
|
269
|
+
],
|
|
270
|
+
execute: async (config) => config.val,
|
|
271
|
+
metric: (result) => result,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
parallel: false,
|
|
275
|
+
onVariantComplete: (result) => {
|
|
276
|
+
completedResults.push(result.metricValue!)
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
expect(completedResults).toEqual([10, 20])
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
describe('error handling', () => {
|
|
286
|
+
it('captures variant errors without stopping experiment', async () => {
|
|
287
|
+
const results = await Experiment({
|
|
288
|
+
id: 'error-test',
|
|
289
|
+
name: 'Error Test',
|
|
290
|
+
variants: [
|
|
291
|
+
{ id: 'success', name: 'Success', config: { fail: false } },
|
|
292
|
+
{ id: 'failure', name: 'Failure', config: { fail: true } },
|
|
293
|
+
{ id: 'success2', name: 'Success 2', config: { fail: false } },
|
|
294
|
+
],
|
|
295
|
+
execute: async (config) => {
|
|
296
|
+
if (config.fail) {
|
|
297
|
+
throw new Error('Intentional failure')
|
|
298
|
+
}
|
|
299
|
+
return 'ok'
|
|
300
|
+
},
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
expect(results.successCount).toBe(2)
|
|
304
|
+
expect(results.failureCount).toBe(1)
|
|
305
|
+
|
|
306
|
+
const failedResult = results.results.find((r) => r.variantId === 'failure')
|
|
307
|
+
expect(failedResult?.success).toBe(false)
|
|
308
|
+
expect(failedResult?.error).toBeInstanceOf(Error)
|
|
309
|
+
expect(failedResult?.error?.message).toBe('Intentional failure')
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('stops on first error when stopOnError=true', async () => {
|
|
313
|
+
const executedVariants: string[] = []
|
|
314
|
+
|
|
315
|
+
const results = await Experiment(
|
|
316
|
+
{
|
|
317
|
+
id: 'stop-on-error-test',
|
|
318
|
+
name: 'Stop on Error Test',
|
|
319
|
+
variants: [
|
|
320
|
+
{ id: 'v1', name: 'V1', config: { fail: false } },
|
|
321
|
+
{ id: 'v2', name: 'V2', config: { fail: true } },
|
|
322
|
+
{ id: 'v3', name: 'V3', config: { fail: false } },
|
|
323
|
+
],
|
|
324
|
+
execute: async (config) => {
|
|
325
|
+
executedVariants.push(config.fail ? 'fail' : 'success')
|
|
326
|
+
if (config.fail) {
|
|
327
|
+
throw new Error('Stop here')
|
|
328
|
+
}
|
|
329
|
+
return 'ok'
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
{ parallel: false, stopOnError: true }
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
// Should have stopped after v2's error
|
|
336
|
+
expect(results.results.length).toBeLessThanOrEqual(2)
|
|
337
|
+
expect(executedVariants).toContain('fail')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('calls onVariantError callback', async () => {
|
|
341
|
+
const errors: { variantId: string; message: string }[] = []
|
|
342
|
+
|
|
343
|
+
await Experiment(
|
|
344
|
+
{
|
|
345
|
+
id: 'error-callback-test',
|
|
346
|
+
name: 'Error Callback Test',
|
|
347
|
+
variants: [
|
|
348
|
+
{ id: 'ok', name: 'OK', config: { fail: false } },
|
|
349
|
+
{ id: 'bad', name: 'Bad', config: { fail: true } },
|
|
350
|
+
],
|
|
351
|
+
execute: async (config) => {
|
|
352
|
+
if (config.fail) {
|
|
353
|
+
throw new Error('Expected error')
|
|
354
|
+
}
|
|
355
|
+
return 'ok'
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
onVariantError: (variantId, error) => {
|
|
360
|
+
errors.push({ variantId, message: error.message })
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
expect(errors).toHaveLength(1)
|
|
366
|
+
expect(errors[0].variantId).toBe('bad')
|
|
367
|
+
expect(errors[0].message).toBe('Expected error')
|
|
368
|
+
})
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
describe('tracking integration', () => {
|
|
372
|
+
it('tracks experiment start and complete events', async () => {
|
|
373
|
+
await Experiment({
|
|
374
|
+
id: 'tracking-test',
|
|
375
|
+
name: 'Tracking Test',
|
|
376
|
+
variants: [{ id: 'v1', name: 'V1', config: {} }],
|
|
377
|
+
execute: async () => 'done',
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
const events = testBackend.getEvents()
|
|
381
|
+
const eventTypes = events.map((e) => e.type)
|
|
382
|
+
|
|
383
|
+
expect(eventTypes).toContain('experiment.start')
|
|
384
|
+
expect(eventTypes).toContain('experiment.complete')
|
|
385
|
+
expect(eventTypes).toContain('variant.start')
|
|
386
|
+
expect(eventTypes).toContain('variant.complete')
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it('tracks metric computation', async () => {
|
|
390
|
+
await Experiment({
|
|
391
|
+
id: 'metric-tracking-test',
|
|
392
|
+
name: 'Metric Tracking Test',
|
|
393
|
+
variants: [{ id: 'v1', name: 'V1', config: {} }],
|
|
394
|
+
execute: async () => 42,
|
|
395
|
+
metric: (result) => result,
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
const events = testBackend.getEvents()
|
|
399
|
+
const metricEvent = events.find((e) => e.type === 'metric.computed')
|
|
400
|
+
|
|
401
|
+
expect(metricEvent).toBeDefined()
|
|
402
|
+
expect(metricEvent?.data.metricValue).toBe(42)
|
|
403
|
+
})
|
|
404
|
+
})
|
|
405
|
+
})
|
|
406
|
+
|
|
407
|
+
describe('createVariantsFromGrid', () => {
|
|
408
|
+
it('creates variants from single parameter', () => {
|
|
409
|
+
const variants = createVariantsFromGrid({
|
|
410
|
+
temperature: [0.3, 0.7, 1.0],
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
expect(variants).toHaveLength(3)
|
|
414
|
+
expect(variants[0].config).toEqual({ temperature: 0.3 })
|
|
415
|
+
expect(variants[1].config).toEqual({ temperature: 0.7 })
|
|
416
|
+
expect(variants[2].config).toEqual({ temperature: 1.0 })
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('creates cartesian product of multiple parameters', () => {
|
|
420
|
+
const variants = createVariantsFromGrid({
|
|
421
|
+
model: ['a', 'b'],
|
|
422
|
+
temperature: [0.3, 0.7],
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
// 2 * 2 = 4 combinations
|
|
426
|
+
expect(variants).toHaveLength(4)
|
|
427
|
+
|
|
428
|
+
const configs = variants.map((v) => v.config)
|
|
429
|
+
expect(configs).toContainEqual({ model: 'a', temperature: 0.3 })
|
|
430
|
+
expect(configs).toContainEqual({ model: 'a', temperature: 0.7 })
|
|
431
|
+
expect(configs).toContainEqual({ model: 'b', temperature: 0.3 })
|
|
432
|
+
expect(configs).toContainEqual({ model: 'b', temperature: 0.7 })
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
it('assigns unique IDs to each variant', () => {
|
|
436
|
+
const variants = createVariantsFromGrid({
|
|
437
|
+
x: [1, 2, 3],
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
const ids = variants.map((v) => v.id)
|
|
441
|
+
const uniqueIds = new Set(ids)
|
|
442
|
+
|
|
443
|
+
expect(uniqueIds.size).toBe(ids.length)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it('generates descriptive names from config values', () => {
|
|
447
|
+
const variants = createVariantsFromGrid({
|
|
448
|
+
model: ['sonnet'],
|
|
449
|
+
temp: [0.5],
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
expect(variants[0].name).toContain('model=sonnet')
|
|
453
|
+
expect(variants[0].name).toContain('temp=0.5')
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('handles three or more parameters', () => {
|
|
457
|
+
const variants = createVariantsFromGrid({
|
|
458
|
+
a: [1, 2],
|
|
459
|
+
b: ['x', 'y'],
|
|
460
|
+
c: [true, false],
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
// 2 * 2 * 2 = 8 combinations
|
|
464
|
+
expect(variants).toHaveLength(8)
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
it('handles empty grid by returning single variant with empty config', () => {
|
|
468
|
+
const variants = createVariantsFromGrid({})
|
|
469
|
+
// The implementation returns a single variant with empty config for empty grid
|
|
470
|
+
expect(variants).toHaveLength(1)
|
|
471
|
+
expect(variants[0].config).toEqual({})
|
|
472
|
+
})
|
|
473
|
+
})
|