ai-tests 2.0.1

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 (47) hide show
  1. package/.turbo/turbo-build.log +5 -0
  2. package/CHANGELOG.md +3 -0
  3. package/README.md +214 -0
  4. package/dist/assertions.d.ts +132 -0
  5. package/dist/assertions.d.ts.map +1 -0
  6. package/dist/assertions.js +384 -0
  7. package/dist/assertions.js.map +1 -0
  8. package/dist/cli.d.ts +13 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +77 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/index.d.ts +16 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +19 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/local.d.ts +31 -0
  17. package/dist/local.d.ts.map +1 -0
  18. package/dist/local.js +63 -0
  19. package/dist/local.js.map +1 -0
  20. package/dist/runner.d.ts +68 -0
  21. package/dist/runner.d.ts.map +1 -0
  22. package/dist/runner.js +215 -0
  23. package/dist/runner.js.map +1 -0
  24. package/dist/types.d.ts +62 -0
  25. package/dist/types.d.ts.map +1 -0
  26. package/dist/types.js +5 -0
  27. package/dist/types.js.map +1 -0
  28. package/dist/worker.d.ts +55 -0
  29. package/dist/worker.d.ts.map +1 -0
  30. package/dist/worker.js +92 -0
  31. package/dist/worker.js.map +1 -0
  32. package/package.json +58 -0
  33. package/src/assertions.ts +465 -0
  34. package/src/cli.ts +87 -0
  35. package/src/index.ts +33 -0
  36. package/src/local.ts +76 -0
  37. package/src/runner.ts +238 -0
  38. package/src/types.ts +68 -0
  39. package/src/worker.ts +112 -0
  40. package/test/assertions.test.ts +607 -0
  41. package/test/index.test.ts +51 -0
  42. package/test/local.test.ts +31 -0
  43. package/test/runner.test.ts +379 -0
  44. package/test/worker.test.ts +198 -0
  45. package/tsconfig.json +23 -0
  46. package/vitest.config.ts +11 -0
  47. package/wrangler.toml +6 -0
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Assertion utilities powered by Chai
3
+ *
4
+ * Exposes expect, should, and assert APIs via RPC.
5
+ * Uses Chai under the hood for battle-tested assertions.
6
+ */
7
+
8
+ import * as chai from 'chai'
9
+ import { RpcTarget } from 'cloudflare:workers'
10
+
11
+ // Initialize chai's should
12
+ chai.should()
13
+
14
+ /**
15
+ * Wrapper around Chai's expect that extends RpcTarget
16
+ * This allows the assertion chain to work over RPC with promise pipelining
17
+ */
18
+ export class Assertion extends RpcTarget {
19
+ // Using 'any' to avoid complex type gymnastics with Chai's chainable types
20
+ // (Deep, Nested, etc. don't match Chai.Assertion directly)
21
+ private assertion: any
22
+
23
+ constructor(value: unknown, message?: string) {
24
+ super()
25
+ this.assertion = chai.expect(value, message)
26
+ }
27
+
28
+ // Chainable language chains
29
+ get to() { return this }
30
+ get be() { return this }
31
+ get been() { return this }
32
+ get is() { return this }
33
+ get that() { return this }
34
+ get which() { return this }
35
+ get and() { return this }
36
+ get has() { return this }
37
+ get have() { return this }
38
+ get with() { return this }
39
+ get at() { return this }
40
+ get of() { return this }
41
+ get same() { return this }
42
+ get but() { return this }
43
+ get does() { return this }
44
+ get still() { return this }
45
+ get also() { return this }
46
+
47
+ // Negation
48
+ get not(): Assertion {
49
+ this.assertion = this.assertion.not
50
+ return this
51
+ }
52
+
53
+ // Deep flag
54
+ get deep(): Assertion {
55
+ this.assertion = this.assertion.deep
56
+ return this
57
+ }
58
+
59
+ // Nested flag
60
+ get nested(): Assertion {
61
+ this.assertion = this.assertion.nested
62
+ return this
63
+ }
64
+
65
+ // Own flag
66
+ get own(): Assertion {
67
+ this.assertion = this.assertion.own
68
+ return this
69
+ }
70
+
71
+ // Ordered flag
72
+ get ordered(): Assertion {
73
+ this.assertion = this.assertion.ordered
74
+ return this
75
+ }
76
+
77
+ // Any flag
78
+ get any(): Assertion {
79
+ this.assertion = this.assertion.any
80
+ return this
81
+ }
82
+
83
+ // All flag
84
+ get all(): Assertion {
85
+ this.assertion = this.assertion.all
86
+ return this
87
+ }
88
+
89
+ // Length chain
90
+ get length(): Assertion {
91
+ this.assertion = this.assertion.length
92
+ return this
93
+ }
94
+
95
+ // Type assertions
96
+ get ok() { this.assertion.ok; return this }
97
+ get true() { this.assertion.true; return this }
98
+ get false() { this.assertion.false; return this }
99
+ get null() { this.assertion.null; return this }
100
+ get undefined() { this.assertion.undefined; return this }
101
+ get NaN() { this.assertion.NaN; return this }
102
+ get exist() { this.assertion.exist; return this }
103
+ get empty() { this.assertion.empty; return this }
104
+ get arguments() { this.assertion.arguments; return this }
105
+
106
+ // Value assertions
107
+ equal(value: unknown, message?: string) {
108
+ this.assertion.equal(value, message)
109
+ return this
110
+ }
111
+
112
+ equals(value: unknown, message?: string) {
113
+ return this.equal(value, message)
114
+ }
115
+
116
+ eq(value: unknown, message?: string) {
117
+ return this.equal(value, message)
118
+ }
119
+
120
+ eql(value: unknown, message?: string) {
121
+ this.assertion.eql(value, message)
122
+ return this
123
+ }
124
+
125
+ eqls(value: unknown, message?: string) {
126
+ return this.eql(value, message)
127
+ }
128
+
129
+ above(value: number, message?: string) {
130
+ this.assertion.above(value, message)
131
+ return this
132
+ }
133
+
134
+ gt(value: number, message?: string) {
135
+ return this.above(value, message)
136
+ }
137
+
138
+ greaterThan(value: number, message?: string) {
139
+ return this.above(value, message)
140
+ }
141
+
142
+ least(value: number, message?: string) {
143
+ this.assertion.least(value, message)
144
+ return this
145
+ }
146
+
147
+ gte(value: number, message?: string) {
148
+ return this.least(value, message)
149
+ }
150
+
151
+ greaterThanOrEqual(value: number, message?: string) {
152
+ return this.least(value, message)
153
+ }
154
+
155
+ below(value: number, message?: string) {
156
+ this.assertion.below(value, message)
157
+ return this
158
+ }
159
+
160
+ lt(value: number, message?: string) {
161
+ return this.below(value, message)
162
+ }
163
+
164
+ lessThan(value: number, message?: string) {
165
+ return this.below(value, message)
166
+ }
167
+
168
+ most(value: number, message?: string) {
169
+ this.assertion.most(value, message)
170
+ return this
171
+ }
172
+
173
+ lte(value: number, message?: string) {
174
+ return this.most(value, message)
175
+ }
176
+
177
+ lessThanOrEqual(value: number, message?: string) {
178
+ return this.most(value, message)
179
+ }
180
+
181
+ within(start: number, finish: number, message?: string) {
182
+ this.assertion.within(start, finish, message)
183
+ return this
184
+ }
185
+
186
+ instanceof(constructor: unknown, message?: string) {
187
+ this.assertion.instanceof(constructor as any, message)
188
+ return this
189
+ }
190
+
191
+ instanceOf(constructor: unknown, message?: string) {
192
+ return this.instanceof(constructor, message)
193
+ }
194
+
195
+ property(name: string, value?: unknown, message?: string) {
196
+ if (arguments.length > 1) {
197
+ this.assertion.property(name, value, message)
198
+ } else {
199
+ this.assertion.property(name)
200
+ }
201
+ return this
202
+ }
203
+
204
+ ownProperty(name: string, message?: string) {
205
+ this.assertion.ownProperty(name, message)
206
+ return this
207
+ }
208
+
209
+ haveOwnProperty(name: string, message?: string) {
210
+ return this.ownProperty(name, message)
211
+ }
212
+
213
+ ownPropertyDescriptor(name: string, descriptor?: PropertyDescriptor, message?: string) {
214
+ this.assertion.ownPropertyDescriptor(name, descriptor, message)
215
+ return this
216
+ }
217
+
218
+ lengthOf(n: number, message?: string) {
219
+ this.assertion.lengthOf(n, message)
220
+ return this
221
+ }
222
+
223
+ match(re: RegExp, message?: string) {
224
+ this.assertion.match(re, message)
225
+ return this
226
+ }
227
+
228
+ matches(re: RegExp, message?: string) {
229
+ return this.match(re, message)
230
+ }
231
+
232
+ string(str: string, message?: string) {
233
+ this.assertion.string(str, message)
234
+ return this
235
+ }
236
+
237
+ keys(...keys: string[]) {
238
+ this.assertion.keys(...keys)
239
+ return this
240
+ }
241
+
242
+ key(...keys: string[]) {
243
+ return this.keys(...keys)
244
+ }
245
+
246
+ throw(errorLike?: unknown, errMsgMatcher?: string | RegExp, message?: string) {
247
+ this.assertion.throw(errorLike as any, errMsgMatcher, message)
248
+ return this
249
+ }
250
+
251
+ throws(errorLike?: unknown, errMsgMatcher?: string | RegExp, message?: string) {
252
+ return this.throw(errorLike, errMsgMatcher, message)
253
+ }
254
+
255
+ Throw(errorLike?: unknown, errMsgMatcher?: string | RegExp, message?: string) {
256
+ return this.throw(errorLike, errMsgMatcher, message)
257
+ }
258
+
259
+ respondTo(method: string, message?: string) {
260
+ this.assertion.respondTo(method, message)
261
+ return this
262
+ }
263
+
264
+ respondsTo(method: string, message?: string) {
265
+ return this.respondTo(method, message)
266
+ }
267
+
268
+ satisfy(matcher: (val: unknown) => boolean, message?: string) {
269
+ this.assertion.satisfy(matcher, message)
270
+ return this
271
+ }
272
+
273
+ satisfies(matcher: (val: unknown) => boolean, message?: string) {
274
+ return this.satisfy(matcher, message)
275
+ }
276
+
277
+ closeTo(expected: number, delta: number, message?: string) {
278
+ this.assertion.closeTo(expected, delta, message)
279
+ return this
280
+ }
281
+
282
+ approximately(expected: number, delta: number, message?: string) {
283
+ return this.closeTo(expected, delta, message)
284
+ }
285
+
286
+ members(set: unknown[], message?: string) {
287
+ this.assertion.members(set, message)
288
+ return this
289
+ }
290
+
291
+ oneOf(list: unknown[], message?: string) {
292
+ this.assertion.oneOf(list, message)
293
+ return this
294
+ }
295
+
296
+ include(value: unknown, message?: string) {
297
+ this.assertion.include(value, message)
298
+ return this
299
+ }
300
+
301
+ includes(value: unknown, message?: string) {
302
+ return this.include(value, message)
303
+ }
304
+
305
+ contain(value: unknown, message?: string) {
306
+ return this.include(value, message)
307
+ }
308
+
309
+ contains(value: unknown, message?: string) {
310
+ return this.include(value, message)
311
+ }
312
+
313
+ a(type: string, message?: string) {
314
+ this.assertion.a(type, message)
315
+ return this
316
+ }
317
+
318
+ an(type: string, message?: string) {
319
+ return this.a(type, message)
320
+ }
321
+
322
+ // Vitest-compatible aliases
323
+ toBe(value: unknown) {
324
+ this.assertion.equal(value)
325
+ return this
326
+ }
327
+
328
+ toEqual(value: unknown) {
329
+ this.assertion.deep.equal(value)
330
+ return this
331
+ }
332
+
333
+ toStrictEqual(value: unknown) {
334
+ this.assertion.deep.equal(value)
335
+ return this
336
+ }
337
+
338
+ toBeTruthy() {
339
+ this.assertion.ok
340
+ return this
341
+ }
342
+
343
+ toBeFalsy() {
344
+ this.assertion.not.ok
345
+ return this
346
+ }
347
+
348
+ toBeNull() {
349
+ this.assertion.null
350
+ return this
351
+ }
352
+
353
+ toBeUndefined() {
354
+ this.assertion.undefined
355
+ return this
356
+ }
357
+
358
+ toBeDefined() {
359
+ this.assertion.not.undefined
360
+ return this
361
+ }
362
+
363
+ toBeNaN() {
364
+ this.assertion.NaN
365
+ return this
366
+ }
367
+
368
+ toContain(value: unknown) {
369
+ this.assertion.include(value)
370
+ return this
371
+ }
372
+
373
+ toHaveLength(length: number) {
374
+ this.assertion.lengthOf(length)
375
+ return this
376
+ }
377
+
378
+ toHaveProperty(path: string, value?: unknown) {
379
+ if (arguments.length > 1) {
380
+ this.assertion.nested.property(path, value)
381
+ } else {
382
+ this.assertion.nested.property(path)
383
+ }
384
+ return this
385
+ }
386
+
387
+ toMatch(pattern: RegExp | string) {
388
+ if (typeof pattern === 'string') {
389
+ this.assertion.include(pattern)
390
+ } else {
391
+ this.assertion.match(pattern)
392
+ }
393
+ return this
394
+ }
395
+
396
+ toMatchObject(obj: object) {
397
+ this.assertion.deep.include(obj)
398
+ return this
399
+ }
400
+
401
+ toThrow(expected?: string | RegExp | Error) {
402
+ if (expected) {
403
+ this.assertion.throw(expected as any)
404
+ } else {
405
+ this.assertion.throw()
406
+ }
407
+ return this
408
+ }
409
+
410
+ toBeGreaterThan(n: number) {
411
+ this.assertion.above(n)
412
+ return this
413
+ }
414
+
415
+ toBeLessThan(n: number) {
416
+ this.assertion.below(n)
417
+ return this
418
+ }
419
+
420
+ toBeGreaterThanOrEqual(n: number) {
421
+ this.assertion.least(n)
422
+ return this
423
+ }
424
+
425
+ toBeLessThanOrEqual(n: number) {
426
+ this.assertion.most(n)
427
+ return this
428
+ }
429
+
430
+ toBeCloseTo(n: number, digits = 2) {
431
+ const delta = Math.pow(10, -digits) / 2
432
+ this.assertion.closeTo(n, delta)
433
+ return this
434
+ }
435
+
436
+ toBeInstanceOf(cls: unknown) {
437
+ this.assertion.instanceof(cls as any)
438
+ return this
439
+ }
440
+
441
+ toBeTypeOf(type: string) {
442
+ this.assertion.a(type)
443
+ return this
444
+ }
445
+ }
446
+
447
+ /**
448
+ * Assert API - TDD style assertions
449
+ */
450
+ export const assert = chai.assert
451
+
452
+ /**
453
+ * Create an expect assertion
454
+ */
455
+ export function expect(value: unknown, message?: string): Assertion {
456
+ return new Assertion(value, message)
457
+ }
458
+
459
+ /**
460
+ * Create a should-style assertion
461
+ * Since we can't modify Object.prototype over RPC, this takes a value
462
+ */
463
+ export function should(value: unknown): Assertion {
464
+ return new Assertion(value)
465
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ai-tests CLI
4
+ *
5
+ * Simple CLI for deploying and managing the ai-tests worker.
6
+ *
7
+ * Usage:
8
+ * npx ai-tests deploy Deploy to Cloudflare Workers
9
+ * npx ai-tests dev Start local dev server
10
+ * npx ai-tests --help Show help
11
+ */
12
+
13
+ import { spawn } from 'node:child_process'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { dirname, join } from 'node:path'
16
+
17
+ const __dirname = dirname(fileURLToPath(import.meta.url))
18
+ const packageRoot = join(__dirname, '..')
19
+
20
+ const commands: Record<string, { description: string; args: string[] }> = {
21
+ deploy: {
22
+ description: 'Deploy ai-tests worker to Cloudflare',
23
+ args: ['wrangler', 'deploy']
24
+ },
25
+ dev: {
26
+ description: 'Start local development server',
27
+ args: ['wrangler', 'dev']
28
+ }
29
+ }
30
+
31
+ function showHelp() {
32
+ console.log(`
33
+ ai-tests - Test utilities worker for Cloudflare Workers
34
+
35
+ Usage:
36
+ npx ai-tests <command>
37
+
38
+ Commands:
39
+ deploy Deploy to Cloudflare Workers
40
+ dev Start local development server
41
+ help Show this help message
42
+
43
+ Examples:
44
+ npx ai-tests deploy
45
+ npx ai-tests dev
46
+
47
+ After deploying, bind the worker to your sandbox worker:
48
+ # In your wrangler.toml:
49
+ [[services]]
50
+ binding = "TEST"
51
+ service = "ai-tests"
52
+ `)
53
+ }
54
+
55
+ async function run() {
56
+ const args = process.argv.slice(2)
57
+ const command = args[0]
58
+
59
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
60
+ showHelp()
61
+ process.exit(0)
62
+ }
63
+
64
+ const cmd = commands[command]
65
+ if (!cmd) {
66
+ console.error(`Unknown command: ${command}`)
67
+ showHelp()
68
+ process.exit(1)
69
+ }
70
+
71
+ console.log(`Running: ${cmd.args.join(' ')}`)
72
+
73
+ const child = spawn('npx', cmd.args, {
74
+ cwd: packageRoot,
75
+ stdio: 'inherit',
76
+ shell: true
77
+ })
78
+
79
+ child.on('close', (code) => {
80
+ process.exit(code ?? 0)
81
+ })
82
+ }
83
+
84
+ run().catch((err) => {
85
+ console.error(err)
86
+ process.exit(1)
87
+ })
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ai-tests - Test utilities via RPC
3
+ *
4
+ * Provides expect, should, assert, and a test runner that can be:
5
+ * - Deployed as a Cloudflare Worker
6
+ * - Run locally via Miniflare
7
+ * - Called via Workers RPC from other workers (like ai-sandbox)
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+
12
+ // Export assertion utilities
13
+ export { Assertion, expect, should, assert } from './assertions.js'
14
+
15
+ // Export test runner
16
+ export { TestRunner, createRunner } from './runner.js'
17
+
18
+ // Export worker service (WorkerEntrypoint and RpcTarget)
19
+ export { TestService, TestService as TestWorker, TestServiceCore } from './worker.js'
20
+
21
+ // Export the default WorkerEntrypoint class
22
+ export { default } from './worker.js'
23
+
24
+ // Export types
25
+ export type {
26
+ TestResult,
27
+ TestResults,
28
+ TestFn,
29
+ HookFn,
30
+ RegisteredTest,
31
+ SuiteContext,
32
+ TestEnv
33
+ } from './types.js'
package/src/local.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Local development helpers for ai-tests
3
+ *
4
+ * Run the test worker via Miniflare for local development and Node.js usage.
5
+ */
6
+
7
+ import { TestServiceCore } from './worker.js'
8
+
9
+ let miniflareInstance: InstanceType<typeof import('miniflare').Miniflare> | null = null
10
+ let localService: TestServiceCore | null = null
11
+
12
+ /**
13
+ * Get a local TestServiceCore instance
14
+ *
15
+ * For local development, this creates a direct instance.
16
+ * The RPC serialization happens when called via worker bindings.
17
+ */
18
+ export function getLocalTestService(): TestServiceCore {
19
+ if (!localService) {
20
+ localService = new TestServiceCore()
21
+ }
22
+ return localService
23
+ }
24
+
25
+ /**
26
+ * Start a Miniflare instance running the test worker
27
+ *
28
+ * This is useful for testing the RPC interface locally.
29
+ */
30
+ export async function startTestWorker(options?: {
31
+ port?: number
32
+ }): Promise<{
33
+ url: string
34
+ stop: () => Promise<void>
35
+ }> {
36
+ const { Miniflare } = await import('miniflare')
37
+
38
+ // Build worker script inline
39
+ const workerScript = `
40
+ import { TestService } from './index.js';
41
+ export { TestService };
42
+ export default {
43
+ async fetch(request) {
44
+ return new Response('ai-tests worker running');
45
+ }
46
+ };
47
+ `
48
+
49
+ miniflareInstance = new Miniflare({
50
+ modules: true,
51
+ script: workerScript,
52
+ port: options?.port,
53
+ compatibilityDate: '2024-12-01',
54
+ })
55
+
56
+ const url = await miniflareInstance.ready
57
+
58
+ return {
59
+ url: url.toString(),
60
+ stop: async () => {
61
+ if (miniflareInstance) {
62
+ await miniflareInstance.dispose()
63
+ miniflareInstance = null
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Create a test service that can be used as a service binding
71
+ *
72
+ * For use in Miniflare configurations when testing sandbox workers.
73
+ */
74
+ export function createTestServiceBinding(): TestServiceCore {
75
+ return new TestServiceCore()
76
+ }