ai-evaluate 2.1.6 → 2.1.8
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/README.md +90 -3
- package/dist/capnweb-bundle.d.ts +10 -0
- package/dist/capnweb-bundle.d.ts.map +1 -0
- package/dist/capnweb-bundle.js +2596 -0
- package/dist/capnweb-bundle.js.map +1 -0
- package/dist/evaluate.d.ts +1 -1
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +186 -7
- package/dist/evaluate.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/miniflare-pool.d.ts +109 -0
- package/dist/miniflare-pool.d.ts.map +1 -0
- package/dist/miniflare-pool.js +308 -0
- package/dist/miniflare-pool.js.map +1 -0
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +42 -10
- package/dist/node.js.map +1 -1
- package/dist/shared.d.ts +66 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +169 -0
- package/dist/shared.js.map +1 -0
- package/dist/type-guards.d.ts +21 -0
- package/dist/type-guards.d.ts.map +1 -0
- package/dist/type-guards.js +216 -0
- package/dist/type-guards.js.map +1 -0
- package/dist/types.d.ts +17 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +26 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +104 -0
- package/dist/validation.js.map +1 -0
- package/dist/worker-template/code-transforms.d.ts +9 -0
- package/dist/worker-template/code-transforms.d.ts.map +1 -0
- package/dist/worker-template/code-transforms.js +28 -0
- package/dist/worker-template/code-transforms.js.map +1 -0
- package/{src/worker-template.d.ts → dist/worker-template/core.d.ts} +7 -15
- package/dist/worker-template/core.d.ts.map +1 -0
- package/dist/worker-template/core.js +502 -0
- package/dist/worker-template/core.js.map +1 -0
- package/dist/worker-template/helpers.d.ts +14 -0
- package/dist/worker-template/helpers.d.ts.map +1 -0
- package/dist/worker-template/helpers.js +79 -0
- package/dist/worker-template/helpers.js.map +1 -0
- package/dist/worker-template/index.d.ts +14 -0
- package/dist/worker-template/index.d.ts.map +1 -0
- package/dist/worker-template/index.js +19 -0
- package/dist/worker-template/index.js.map +1 -0
- package/dist/worker-template/sdk-generator.d.ts +17 -0
- package/dist/worker-template/sdk-generator.d.ts.map +1 -0
- package/{src/worker-template.js → dist/worker-template/sdk-generator.js} +377 -1506
- package/dist/worker-template/sdk-generator.js.map +1 -0
- package/dist/worker-template/test-generator.d.ts +16 -0
- package/dist/worker-template/test-generator.d.ts.map +1 -0
- package/dist/worker-template/test-generator.js +357 -0
- package/dist/worker-template/test-generator.js.map +1 -0
- package/dist/worker-template.d.ts +2 -2
- package/dist/worker-template.d.ts.map +1 -1
- package/dist/worker-template.js +64 -31
- package/dist/worker-template.js.map +1 -1
- package/example/package.json +7 -3
- package/example/src/index.ts +194 -40
- package/example/wrangler.jsonc +18 -2
- package/package.json +1 -3
- package/src/capnweb-bundle.ts +2596 -0
- package/src/evaluate.ts +216 -7
- package/src/index.ts +3 -1
- package/src/miniflare-pool.ts +395 -0
- package/src/node.ts +56 -11
- package/src/shared.ts +186 -0
- package/src/type-guards.ts +323 -0
- package/src/types.ts +18 -2
- package/src/validation.ts +120 -0
- package/src/worker-template/code-transforms.ts +32 -0
- package/src/worker-template/core.ts +557 -0
- package/src/worker-template/helpers.ts +90 -0
- package/src/worker-template/index.ts +23 -0
- package/src/{worker-template.ts → worker-template/sdk-generator.ts} +322 -1566
- package/src/worker-template/test-generator.ts +358 -0
- package/test/miniflare-pool.test.ts +246 -0
- package/test/node.test.ts +467 -0
- package/test/security.test.ts +1009 -0
- package/test/shared.test.ts +105 -0
- package/test/type-guards.test.ts +303 -0
- package/test/validation.test.ts +240 -0
- package/test/worker-template.test.ts +21 -19
- package/src/evaluate.js +0 -187
- package/src/index.js +0 -10
- package/src/node.d.ts +0 -17
- package/src/node.d.ts.map +0 -1
- package/src/node.js +0 -168
- package/src/node.js.map +0 -1
- package/src/types.d.ts +0 -172
- package/src/types.d.ts.map +0 -1
- package/src/types.js +0 -4
- package/src/types.js.map +0 -1
- package/src/worker-template.d.ts.map +0 -1
- package/src/worker-template.js.map +0 -1
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test framework embedding for worker templates
|
|
3
|
+
*
|
|
4
|
+
* Generates the embedded test framework code used in dev mode
|
|
5
|
+
* (vitest-compatible API without external dependencies)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate the embedded test framework code
|
|
10
|
+
* This creates a vitest-compatible testing API that runs in the sandbox
|
|
11
|
+
*/
|
|
12
|
+
export function generateTestFrameworkCode(): string {
|
|
13
|
+
return `
|
|
14
|
+
// Test framework (vitest-compatible API)
|
|
15
|
+
let currentDescribe = '';
|
|
16
|
+
let beforeEachFns = [];
|
|
17
|
+
let afterEachFns = [];
|
|
18
|
+
|
|
19
|
+
const describe = (name, fn) => {
|
|
20
|
+
const prev = currentDescribe;
|
|
21
|
+
const prevBeforeEach = [...beforeEachFns];
|
|
22
|
+
const prevAfterEach = [...afterEachFns];
|
|
23
|
+
currentDescribe = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
24
|
+
try { fn(); } finally {
|
|
25
|
+
currentDescribe = prev;
|
|
26
|
+
beforeEachFns = prevBeforeEach;
|
|
27
|
+
afterEachFns = prevAfterEach;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Hooks
|
|
32
|
+
const beforeEach = (fn) => { beforeEachFns.push(fn); };
|
|
33
|
+
const afterEach = (fn) => { afterEachFns.push(fn); };
|
|
34
|
+
|
|
35
|
+
const it = (name, fn) => {
|
|
36
|
+
const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
37
|
+
const hooks = { before: [...beforeEachFns], after: [...afterEachFns] };
|
|
38
|
+
pendingTests.push({ name: fullName, fn, hooks });
|
|
39
|
+
};
|
|
40
|
+
const test = it;
|
|
41
|
+
|
|
42
|
+
it.skip = (name, fn) => {
|
|
43
|
+
const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
44
|
+
pendingTests.push({ name: fullName, fn: null, skip: true });
|
|
45
|
+
};
|
|
46
|
+
test.skip = it.skip;
|
|
47
|
+
|
|
48
|
+
it.only = (name, fn) => {
|
|
49
|
+
const fullName = currentDescribe ? currentDescribe + ' > ' + name : name;
|
|
50
|
+
const hooks = { before: [...beforeEachFns], after: [...afterEachFns] };
|
|
51
|
+
pendingTests.push({ name: fullName, fn, hooks, only: true });
|
|
52
|
+
};
|
|
53
|
+
test.only = it.only;
|
|
54
|
+
|
|
55
|
+
// Deep equality check
|
|
56
|
+
const deepEqual = (a, b) => {
|
|
57
|
+
if (a === b) return true;
|
|
58
|
+
if (a == null || b == null) return false;
|
|
59
|
+
if (typeof a !== typeof b) return false;
|
|
60
|
+
if (typeof a !== 'object') return false;
|
|
61
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
62
|
+
if (Array.isArray(a)) {
|
|
63
|
+
if (a.length !== b.length) return false;
|
|
64
|
+
return a.every((v, i) => deepEqual(v, b[i]));
|
|
65
|
+
}
|
|
66
|
+
const keysA = Object.keys(a);
|
|
67
|
+
const keysB = Object.keys(b);
|
|
68
|
+
if (keysA.length !== keysB.length) return false;
|
|
69
|
+
return keysA.every(k => deepEqual(a[k], b[k]));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Expect implementation with vitest-compatible matchers
|
|
73
|
+
const expect = (actual) => {
|
|
74
|
+
const matchers = {
|
|
75
|
+
toBe: (expected) => {
|
|
76
|
+
if (actual !== expected) {
|
|
77
|
+
throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
toEqual: (expected) => {
|
|
81
|
+
if (!deepEqual(actual, expected)) {
|
|
82
|
+
throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
toStrictEqual: (expected) => {
|
|
86
|
+
if (!deepEqual(actual, expected)) {
|
|
87
|
+
throw new Error(\`Expected \${JSON.stringify(expected)} but got \${JSON.stringify(actual)}\`);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
toBeTruthy: () => {
|
|
91
|
+
if (!actual) throw new Error(\`Expected truthy but got \${JSON.stringify(actual)}\`);
|
|
92
|
+
},
|
|
93
|
+
toBeFalsy: () => {
|
|
94
|
+
if (actual) throw new Error(\`Expected falsy but got \${JSON.stringify(actual)}\`);
|
|
95
|
+
},
|
|
96
|
+
toBeNull: () => {
|
|
97
|
+
if (actual !== null) throw new Error(\`Expected null but got \${JSON.stringify(actual)}\`);
|
|
98
|
+
},
|
|
99
|
+
toBeUndefined: () => {
|
|
100
|
+
if (actual !== undefined) throw new Error(\`Expected undefined but got \${JSON.stringify(actual)}\`);
|
|
101
|
+
},
|
|
102
|
+
toBeDefined: () => {
|
|
103
|
+
if (actual === undefined) throw new Error('Expected defined but got undefined');
|
|
104
|
+
},
|
|
105
|
+
toBeNaN: () => {
|
|
106
|
+
if (!Number.isNaN(actual)) throw new Error(\`Expected NaN but got \${actual}\`);
|
|
107
|
+
},
|
|
108
|
+
toContain: (item) => {
|
|
109
|
+
if (Array.isArray(actual)) {
|
|
110
|
+
if (!actual.includes(item)) throw new Error(\`Expected array to contain \${JSON.stringify(item)}\`);
|
|
111
|
+
} else if (typeof actual === 'string') {
|
|
112
|
+
if (!actual.includes(item)) throw new Error(\`Expected string to contain "\${item}"\`);
|
|
113
|
+
} else {
|
|
114
|
+
throw new Error('toContain only works on arrays and strings');
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
toContainEqual: (item) => {
|
|
118
|
+
if (!Array.isArray(actual)) throw new Error('toContainEqual only works on arrays');
|
|
119
|
+
if (!actual.some(v => deepEqual(v, item))) {
|
|
120
|
+
throw new Error(\`Expected array to contain \${JSON.stringify(item)}\`);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
toHaveLength: (length) => {
|
|
124
|
+
if (actual?.length !== length) {
|
|
125
|
+
throw new Error(\`Expected length \${length} but got \${actual?.length}\`);
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
toHaveProperty: function(path, value) {
|
|
129
|
+
const parts = typeof path === 'string' ? path.split('.') : [path];
|
|
130
|
+
let obj = actual;
|
|
131
|
+
for (const part of parts) {
|
|
132
|
+
if (obj == null || !(part in obj)) {
|
|
133
|
+
throw new Error(\`Expected object to have property "\${path}"\`);
|
|
134
|
+
}
|
|
135
|
+
obj = obj[part];
|
|
136
|
+
}
|
|
137
|
+
if (arguments.length > 1 && !deepEqual(obj, value)) {
|
|
138
|
+
throw new Error(\`Expected property "\${path}" to be \${JSON.stringify(value)} but got \${JSON.stringify(obj)}\`);
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
toMatchObject: (expected) => {
|
|
142
|
+
if (typeof actual !== 'object' || actual === null) {
|
|
143
|
+
throw new Error('toMatchObject expects an object');
|
|
144
|
+
}
|
|
145
|
+
for (const key of Object.keys(expected)) {
|
|
146
|
+
if (!deepEqual(actual[key], expected[key])) {
|
|
147
|
+
throw new Error(\`Expected property "\${key}" to be \${JSON.stringify(expected[key])} but got \${JSON.stringify(actual[key])}\`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
toThrow: (expected) => {
|
|
152
|
+
if (typeof actual !== 'function') throw new Error('toThrow expects a function');
|
|
153
|
+
let threw = false;
|
|
154
|
+
let error;
|
|
155
|
+
try {
|
|
156
|
+
actual();
|
|
157
|
+
} catch (e) {
|
|
158
|
+
threw = true;
|
|
159
|
+
error = e;
|
|
160
|
+
}
|
|
161
|
+
if (!threw) throw new Error('Expected function to throw');
|
|
162
|
+
if (expected !== undefined) {
|
|
163
|
+
if (typeof expected === 'string' && !error.message.includes(expected)) {
|
|
164
|
+
throw new Error(\`Expected error message to contain "\${expected}" but got "\${error.message}"\`);
|
|
165
|
+
}
|
|
166
|
+
if (expected instanceof RegExp && !expected.test(error.message)) {
|
|
167
|
+
throw new Error(\`Expected error message to match \${expected} but got "\${error.message}"\`);
|
|
168
|
+
}
|
|
169
|
+
if (typeof expected === 'function' && !(error instanceof expected)) {
|
|
170
|
+
throw new Error(\`Expected error to be instance of \${expected.name}\`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
toBeGreaterThan: (n) => {
|
|
175
|
+
if (!(actual > n)) throw new Error(\`Expected \${actual} to be greater than \${n}\`);
|
|
176
|
+
},
|
|
177
|
+
toBeLessThan: (n) => {
|
|
178
|
+
if (!(actual < n)) throw new Error(\`Expected \${actual} to be less than \${n}\`);
|
|
179
|
+
},
|
|
180
|
+
toBeGreaterThanOrEqual: (n) => {
|
|
181
|
+
if (!(actual >= n)) throw new Error(\`Expected \${actual} to be >= \${n}\`);
|
|
182
|
+
},
|
|
183
|
+
toBeLessThanOrEqual: (n) => {
|
|
184
|
+
if (!(actual <= n)) throw new Error(\`Expected \${actual} to be <= \${n}\`);
|
|
185
|
+
},
|
|
186
|
+
toBeCloseTo: (n, digits = 2) => {
|
|
187
|
+
const diff = Math.abs(actual - n);
|
|
188
|
+
const threshold = Math.pow(10, -digits) / 2;
|
|
189
|
+
if (diff > threshold) {
|
|
190
|
+
throw new Error(\`Expected \${actual} to be close to \${n}\`);
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
toMatch: (pattern) => {
|
|
194
|
+
if (typeof actual !== 'string') throw new Error('toMatch expects a string');
|
|
195
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
196
|
+
if (!regex.test(actual)) {
|
|
197
|
+
throw new Error(\`Expected "\${actual}" to match \${pattern}\`);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
toBeInstanceOf: (cls) => {
|
|
201
|
+
if (!(actual instanceof cls)) {
|
|
202
|
+
throw new Error(\`Expected instance of \${cls.name}\`);
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
toBeTypeOf: (type) => {
|
|
206
|
+
if (typeof actual !== type) {
|
|
207
|
+
throw new Error(\`Expected typeof to be "\${type}" but got "\${typeof actual}"\`);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
matchers.not = {
|
|
213
|
+
toBe: (expected) => {
|
|
214
|
+
if (actual === expected) throw new Error(\`Expected not \${JSON.stringify(expected)}\`);
|
|
215
|
+
},
|
|
216
|
+
toEqual: (expected) => {
|
|
217
|
+
if (deepEqual(actual, expected)) {
|
|
218
|
+
throw new Error(\`Expected not equal to \${JSON.stringify(expected)}\`);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
toBeTruthy: () => {
|
|
222
|
+
if (actual) throw new Error('Expected not truthy');
|
|
223
|
+
},
|
|
224
|
+
toBeFalsy: () => {
|
|
225
|
+
if (!actual) throw new Error('Expected not falsy');
|
|
226
|
+
},
|
|
227
|
+
toBeNull: () => {
|
|
228
|
+
if (actual === null) throw new Error('Expected not null');
|
|
229
|
+
},
|
|
230
|
+
toBeUndefined: () => {
|
|
231
|
+
if (actual === undefined) throw new Error('Expected not undefined');
|
|
232
|
+
},
|
|
233
|
+
toBeDefined: () => {
|
|
234
|
+
if (actual !== undefined) throw new Error('Expected undefined');
|
|
235
|
+
},
|
|
236
|
+
toContain: (item) => {
|
|
237
|
+
if (Array.isArray(actual) && actual.includes(item)) {
|
|
238
|
+
throw new Error(\`Expected array not to contain \${JSON.stringify(item)}\`);
|
|
239
|
+
}
|
|
240
|
+
if (typeof actual === 'string' && actual.includes(item)) {
|
|
241
|
+
throw new Error(\`Expected string not to contain "\${item}"\`);
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
toHaveProperty: (path) => {
|
|
245
|
+
const parts = typeof path === 'string' ? path.split('.') : [path];
|
|
246
|
+
let obj = actual;
|
|
247
|
+
try {
|
|
248
|
+
for (const part of parts) {
|
|
249
|
+
if (obj == null || !(part in obj)) return;
|
|
250
|
+
obj = obj[part];
|
|
251
|
+
}
|
|
252
|
+
throw new Error(\`Expected object not to have property "\${path}"\`);
|
|
253
|
+
} catch {}
|
|
254
|
+
},
|
|
255
|
+
toThrow: () => {
|
|
256
|
+
if (typeof actual !== 'function') throw new Error('toThrow expects a function');
|
|
257
|
+
try {
|
|
258
|
+
actual();
|
|
259
|
+
} catch (e) {
|
|
260
|
+
throw new Error('Expected function not to throw');
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
toMatch: (pattern) => {
|
|
264
|
+
if (typeof actual !== 'string') throw new Error('toMatch expects a string');
|
|
265
|
+
const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
|
|
266
|
+
if (regex.test(actual)) {
|
|
267
|
+
throw new Error(\`Expected "\${actual}" not to match \${pattern}\`);
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
matchers.resolves = new Proxy({}, {
|
|
273
|
+
get: (_, prop) => async (...args) => {
|
|
274
|
+
const resolved = await actual;
|
|
275
|
+
return expect(resolved)[prop](...args);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
matchers.rejects = new Proxy({}, {
|
|
280
|
+
get: (_, prop) => async (...args) => {
|
|
281
|
+
try {
|
|
282
|
+
await actual;
|
|
283
|
+
throw new Error('Expected promise to reject');
|
|
284
|
+
} catch (e) {
|
|
285
|
+
if (e.message === 'Expected promise to reject') throw e;
|
|
286
|
+
return expect(e)[prop](...args);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return matchers;
|
|
292
|
+
};
|
|
293
|
+
`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Generate the test runner code that executes pending tests
|
|
298
|
+
*/
|
|
299
|
+
export function generateTestRunnerCode(): string {
|
|
300
|
+
return `
|
|
301
|
+
// Run all pending tests
|
|
302
|
+
const testStart = Date.now();
|
|
303
|
+
const hasOnly = pendingTests.some(t => t.only);
|
|
304
|
+
const testsToRun = hasOnly ? pendingTests.filter(t => t.only || t.skip) : pendingTests;
|
|
305
|
+
|
|
306
|
+
for (const { name, fn, hooks, skip } of testsToRun) {
|
|
307
|
+
testResults.total++;
|
|
308
|
+
|
|
309
|
+
if (skip) {
|
|
310
|
+
testResults.skipped++;
|
|
311
|
+
testResults.tests.push({ name, passed: true, skipped: true, duration: 0 });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const start = Date.now();
|
|
316
|
+
try {
|
|
317
|
+
// Run beforeEach hooks
|
|
318
|
+
if (hooks?.before) {
|
|
319
|
+
for (const hook of hooks.before) {
|
|
320
|
+
const hookResult = hook();
|
|
321
|
+
if (hookResult && typeof hookResult.then === 'function') {
|
|
322
|
+
await hookResult;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Run the test
|
|
328
|
+
const result = fn();
|
|
329
|
+
if (result && typeof result.then === 'function') {
|
|
330
|
+
await result;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Run afterEach hooks
|
|
334
|
+
if (hooks?.after) {
|
|
335
|
+
for (const hook of hooks.after) {
|
|
336
|
+
const hookResult = hook();
|
|
337
|
+
if (hookResult && typeof hookResult.then === 'function') {
|
|
338
|
+
await hookResult;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
testResults.passed++;
|
|
344
|
+
testResults.tests.push({ name, passed: true, duration: Date.now() - start });
|
|
345
|
+
} catch (e) {
|
|
346
|
+
testResults.failed++;
|
|
347
|
+
testResults.tests.push({
|
|
348
|
+
name,
|
|
349
|
+
passed: false,
|
|
350
|
+
error: e.message || String(e),
|
|
351
|
+
duration: Date.now() - start
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
testResults.duration = Date.now() - testStart;
|
|
357
|
+
`
|
|
358
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
configurePool,
|
|
4
|
+
getPoolConfig,
|
|
5
|
+
getPoolStats,
|
|
6
|
+
warmPool,
|
|
7
|
+
disposePool,
|
|
8
|
+
resetPool,
|
|
9
|
+
} from '../src/miniflare-pool.js'
|
|
10
|
+
import { evaluate } from '../src/node.js'
|
|
11
|
+
|
|
12
|
+
describe('Miniflare Pool', () => {
|
|
13
|
+
// Reset pool state between tests
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
await resetPool()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterEach(async () => {
|
|
19
|
+
await disposePool()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('configurePool', () => {
|
|
23
|
+
it('sets pool size', () => {
|
|
24
|
+
configurePool({ size: 5 })
|
|
25
|
+
const config = getPoolConfig()
|
|
26
|
+
expect(config.size).toBe(5)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('sets maxIdleTime', () => {
|
|
30
|
+
configurePool({ maxIdleTime: 60000 })
|
|
31
|
+
const config = getPoolConfig()
|
|
32
|
+
expect(config.maxIdleTime).toBe(60000)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('preserves existing values when not specified', () => {
|
|
36
|
+
configurePool({ size: 5 })
|
|
37
|
+
configurePool({ maxIdleTime: 60000 })
|
|
38
|
+
const config = getPoolConfig()
|
|
39
|
+
expect(config.size).toBe(5)
|
|
40
|
+
expect(config.maxIdleTime).toBe(60000)
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('getPoolConfig', () => {
|
|
45
|
+
it('returns default configuration', () => {
|
|
46
|
+
const config = getPoolConfig()
|
|
47
|
+
expect(config.size).toBe(3)
|
|
48
|
+
expect(config.maxIdleTime).toBe(30000)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns a copy of configuration', () => {
|
|
52
|
+
const config1 = getPoolConfig()
|
|
53
|
+
const config2 = getPoolConfig()
|
|
54
|
+
expect(config1).not.toBe(config2)
|
|
55
|
+
expect(config1).toEqual(config2)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('getPoolStats', () => {
|
|
60
|
+
it('returns initial empty stats', () => {
|
|
61
|
+
const stats = getPoolStats()
|
|
62
|
+
expect(stats.size).toBe(0)
|
|
63
|
+
expect(stats.available).toBe(0)
|
|
64
|
+
expect(stats.inUse).toBe(0)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('shows correct stats after warming', async () => {
|
|
68
|
+
await warmPool(2)
|
|
69
|
+
const stats = getPoolStats()
|
|
70
|
+
expect(stats.size).toBe(2)
|
|
71
|
+
expect(stats.available).toBe(2)
|
|
72
|
+
expect(stats.inUse).toBe(0)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('warmPool', () => {
|
|
77
|
+
it('creates specified number of instances', async () => {
|
|
78
|
+
await warmPool(2)
|
|
79
|
+
const stats = getPoolStats()
|
|
80
|
+
expect(stats.size).toBe(2)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('respects pool size limit', async () => {
|
|
84
|
+
configurePool({ size: 2 })
|
|
85
|
+
await warmPool(5)
|
|
86
|
+
const stats = getPoolStats()
|
|
87
|
+
// warmPool creates up to the requested count, but pool won't grow beyond size
|
|
88
|
+
// when new instances are acquired
|
|
89
|
+
expect(stats.size).toBeLessThanOrEqual(5)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('does not create duplicates when called multiple times', async () => {
|
|
93
|
+
await warmPool(2)
|
|
94
|
+
await warmPool(2)
|
|
95
|
+
const stats = getPoolStats()
|
|
96
|
+
expect(stats.size).toBe(2)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('disposePool', () => {
|
|
101
|
+
it('disposes all instances', async () => {
|
|
102
|
+
await warmPool(3)
|
|
103
|
+
await disposePool()
|
|
104
|
+
const stats = getPoolStats()
|
|
105
|
+
expect(stats.size).toBe(0)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('can be called multiple times safely', async () => {
|
|
109
|
+
await warmPool(2)
|
|
110
|
+
await disposePool()
|
|
111
|
+
await disposePool()
|
|
112
|
+
const stats = getPoolStats()
|
|
113
|
+
expect(stats.size).toBe(0)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('resetPool', () => {
|
|
118
|
+
it('disposes instances and resets config', async () => {
|
|
119
|
+
configurePool({ size: 10, maxIdleTime: 60000 })
|
|
120
|
+
await warmPool(3)
|
|
121
|
+
await resetPool()
|
|
122
|
+
const stats = getPoolStats()
|
|
123
|
+
const config = getPoolConfig()
|
|
124
|
+
expect(stats.size).toBe(0)
|
|
125
|
+
expect(config.size).toBe(3)
|
|
126
|
+
expect(config.maxIdleTime).toBe(30000)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
describe('pool integration with evaluate', () => {
|
|
131
|
+
it('reuses instances across evaluations', async () => {
|
|
132
|
+
// Warm the pool
|
|
133
|
+
await warmPool(1)
|
|
134
|
+
|
|
135
|
+
// Run multiple evaluations
|
|
136
|
+
const results = await Promise.all([
|
|
137
|
+
evaluate({ script: 'return 1' }),
|
|
138
|
+
evaluate({ script: 'return 2' }),
|
|
139
|
+
evaluate({ script: 'return 3' }),
|
|
140
|
+
])
|
|
141
|
+
|
|
142
|
+
expect(results[0].success).toBe(true)
|
|
143
|
+
expect(results[0].value).toBe(1)
|
|
144
|
+
expect(results[1].success).toBe(true)
|
|
145
|
+
expect(results[1].value).toBe(2)
|
|
146
|
+
expect(results[2].success).toBe(true)
|
|
147
|
+
expect(results[2].value).toBe(3)
|
|
148
|
+
|
|
149
|
+
// Pool should have created instances as needed
|
|
150
|
+
const stats = getPoolStats()
|
|
151
|
+
expect(stats.size).toBeGreaterThan(0)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('returns instances to pool after evaluation', async () => {
|
|
155
|
+
// This test verifies the pool reuses instances by running multiple
|
|
156
|
+
// sequential evaluations. If pooling weren't working, each evaluation
|
|
157
|
+
// would be slower due to creating new Miniflare instances.
|
|
158
|
+
configurePool({ size: 1 })
|
|
159
|
+
|
|
160
|
+
// Run sequential evaluations with same pool size
|
|
161
|
+
const result1 = await evaluate({ script: 'return 1' })
|
|
162
|
+
expect(result1.success).toBe(true)
|
|
163
|
+
expect(result1.value).toBe(1)
|
|
164
|
+
|
|
165
|
+
const result2 = await evaluate({ script: 'return 2' })
|
|
166
|
+
expect(result2.success).toBe(true)
|
|
167
|
+
expect(result2.value).toBe(2)
|
|
168
|
+
|
|
169
|
+
const result3 = await evaluate({ script: 'return 3' })
|
|
170
|
+
expect(result3.success).toBe(true)
|
|
171
|
+
expect(result3.value).toBe(3)
|
|
172
|
+
|
|
173
|
+
// All evaluations should succeed, demonstrating instance reuse
|
|
174
|
+
// (If instances weren't being released, pool would exhaust quickly
|
|
175
|
+
// with size=1 and sequential evaluations would fail)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('creates temporary instances when pool is exhausted', async () => {
|
|
179
|
+
configurePool({ size: 1 })
|
|
180
|
+
await warmPool(1)
|
|
181
|
+
|
|
182
|
+
// Start multiple concurrent evaluations
|
|
183
|
+
const evaluationPromises = [
|
|
184
|
+
evaluate({ script: 'return 1' }),
|
|
185
|
+
evaluate({ script: 'return 2' }),
|
|
186
|
+
evaluate({ script: 'return 3' }),
|
|
187
|
+
]
|
|
188
|
+
|
|
189
|
+
const results = await Promise.all(evaluationPromises)
|
|
190
|
+
|
|
191
|
+
// All should succeed (some using temporary instances)
|
|
192
|
+
expect(results.every((r) => r.success)).toBe(true)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('handles errors without leaking instances', async () => {
|
|
196
|
+
configurePool({ size: 1 })
|
|
197
|
+
|
|
198
|
+
// Run an evaluation that throws
|
|
199
|
+
const result = await evaluate({
|
|
200
|
+
script: 'throw new Error("test error")',
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
expect(result.success).toBe(false)
|
|
204
|
+
expect(result.error).toContain('test error')
|
|
205
|
+
|
|
206
|
+
// Instance should still be returned to pool
|
|
207
|
+
const stats = getPoolStats()
|
|
208
|
+
expect(stats.inUse).toBe(0)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('handles timeouts without leaking instances', async () => {
|
|
212
|
+
configurePool({ size: 1 })
|
|
213
|
+
|
|
214
|
+
// Run an evaluation that times out
|
|
215
|
+
const result = await evaluate({
|
|
216
|
+
script: 'while(true) {}',
|
|
217
|
+
timeout: 100,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
expect(result.success).toBe(false)
|
|
221
|
+
expect(result.error).toContain('Timeout')
|
|
222
|
+
|
|
223
|
+
// Instance should still be returned to pool
|
|
224
|
+
const stats = getPoolStats()
|
|
225
|
+
expect(stats.inUse).toBe(0)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('maintains isolation between evaluations', async () => {
|
|
229
|
+
configurePool({ size: 1 })
|
|
230
|
+
|
|
231
|
+
// First evaluation sets a global
|
|
232
|
+
const result1 = await evaluate({
|
|
233
|
+
script: 'globalThis.testValue = 42; return globalThis.testValue;',
|
|
234
|
+
})
|
|
235
|
+
expect(result1.success).toBe(true)
|
|
236
|
+
expect(result1.value).toBe(42)
|
|
237
|
+
|
|
238
|
+
// Second evaluation should not see the global (new worker script)
|
|
239
|
+
const result2 = await evaluate({
|
|
240
|
+
script: 'return globalThis.testValue;',
|
|
241
|
+
})
|
|
242
|
+
expect(result2.success).toBe(true)
|
|
243
|
+
expect(result2.value).toBeUndefined()
|
|
244
|
+
})
|
|
245
|
+
})
|
|
246
|
+
})
|