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
package/src/runner.ts ADDED
@@ -0,0 +1,238 @@
1
+ /**
2
+ * Test runner - describe, it, test, hooks
3
+ *
4
+ * Provides vitest-compatible test runner API via RPC.
5
+ */
6
+
7
+ import { RpcTarget } from 'cloudflare:workers'
8
+ import type { TestResult, TestResults, TestFn, HookFn, RegisteredTest } from './types.js'
9
+
10
+ /**
11
+ * Test runner that collects and runs tests
12
+ */
13
+ export class TestRunner extends RpcTarget {
14
+ private tests: RegisteredTest[] = []
15
+ private currentSuite: string = ''
16
+ private beforeEachHooks: HookFn[] = []
17
+ private afterEachHooks: HookFn[] = []
18
+ private beforeAllHooks: HookFn[] = []
19
+ private afterAllHooks: HookFn[] = []
20
+
21
+ /**
22
+ * Define a test suite
23
+ */
24
+ describe(name: string, fn: () => void): void {
25
+ const prevSuite = this.currentSuite
26
+ const prevBeforeEach = [...this.beforeEachHooks]
27
+ const prevAfterEach = [...this.afterEachHooks]
28
+
29
+ this.currentSuite = this.currentSuite ? `${this.currentSuite} > ${name}` : name
30
+
31
+ try {
32
+ fn()
33
+ } finally {
34
+ this.currentSuite = prevSuite
35
+ this.beforeEachHooks = prevBeforeEach
36
+ this.afterEachHooks = prevAfterEach
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Define a test
42
+ */
43
+ it(name: string, fn: TestFn): void {
44
+ const fullName = this.currentSuite ? `${this.currentSuite} > ${name}` : name
45
+ this.tests.push({
46
+ name: fullName,
47
+ fn,
48
+ hooks: {
49
+ before: [...this.beforeEachHooks],
50
+ after: [...this.afterEachHooks]
51
+ }
52
+ })
53
+ }
54
+
55
+ /**
56
+ * Alias for it
57
+ */
58
+ test(name: string, fn: TestFn): void {
59
+ this.it(name, fn)
60
+ }
61
+
62
+ /**
63
+ * Skip a test
64
+ */
65
+ skip(name: string, _fn?: TestFn): void {
66
+ const fullName = this.currentSuite ? `${this.currentSuite} > ${name}` : name
67
+ this.tests.push({
68
+ name: fullName,
69
+ fn: null,
70
+ hooks: { before: [], after: [] },
71
+ skip: true
72
+ })
73
+ }
74
+
75
+ /**
76
+ * Run only this test
77
+ */
78
+ only(name: string, fn: TestFn): void {
79
+ const fullName = this.currentSuite ? `${this.currentSuite} > ${name}` : name
80
+ this.tests.push({
81
+ name: fullName,
82
+ fn,
83
+ hooks: {
84
+ before: [...this.beforeEachHooks],
85
+ after: [...this.afterEachHooks]
86
+ },
87
+ only: true
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Register a beforeEach hook
93
+ */
94
+ beforeEach(fn: HookFn): void {
95
+ this.beforeEachHooks.push(fn)
96
+ }
97
+
98
+ /**
99
+ * Register an afterEach hook
100
+ */
101
+ afterEach(fn: HookFn): void {
102
+ this.afterEachHooks.push(fn)
103
+ }
104
+
105
+ /**
106
+ * Register a beforeAll hook
107
+ */
108
+ beforeAll(fn: HookFn): void {
109
+ this.beforeAllHooks.push(fn)
110
+ }
111
+
112
+ /**
113
+ * Register an afterAll hook
114
+ */
115
+ afterAll(fn: HookFn): void {
116
+ this.afterAllHooks.push(fn)
117
+ }
118
+
119
+ /**
120
+ * Run all registered tests and return results
121
+ */
122
+ async run(): Promise<TestResults> {
123
+ const startTime = Date.now()
124
+ const results: TestResult[] = []
125
+
126
+ // Check for .only tests
127
+ const hasOnly = this.tests.some(t => t.only)
128
+ const testsToRun = hasOnly
129
+ ? this.tests.filter(t => t.only || t.skip)
130
+ : this.tests
131
+
132
+ // Run beforeAll hooks
133
+ for (const hook of this.beforeAllHooks) {
134
+ try {
135
+ await hook()
136
+ } catch (e) {
137
+ // If beforeAll fails, fail all tests
138
+ const error = e instanceof Error ? e.message : String(e)
139
+ for (const test of testsToRun) {
140
+ results.push({
141
+ name: test.name,
142
+ passed: false,
143
+ error: `beforeAll hook failed: ${error}`,
144
+ duration: 0
145
+ })
146
+ }
147
+ return this.buildResults(results, startTime)
148
+ }
149
+ }
150
+
151
+ // Run each test
152
+ for (const test of testsToRun) {
153
+ if (test.skip) {
154
+ results.push({
155
+ name: test.name,
156
+ passed: true,
157
+ skipped: true,
158
+ duration: 0
159
+ })
160
+ continue
161
+ }
162
+
163
+ const testStart = Date.now()
164
+
165
+ try {
166
+ // Run beforeEach hooks
167
+ for (const hook of test.hooks.before) {
168
+ await hook()
169
+ }
170
+
171
+ // Run the test
172
+ if (test.fn) {
173
+ await test.fn()
174
+ }
175
+
176
+ // Run afterEach hooks
177
+ for (const hook of test.hooks.after) {
178
+ await hook()
179
+ }
180
+
181
+ results.push({
182
+ name: test.name,
183
+ passed: true,
184
+ duration: Date.now() - testStart
185
+ })
186
+ } catch (e) {
187
+ results.push({
188
+ name: test.name,
189
+ passed: false,
190
+ error: e instanceof Error ? e.message : String(e),
191
+ duration: Date.now() - testStart
192
+ })
193
+ }
194
+ }
195
+
196
+ // Run afterAll hooks
197
+ for (const hook of this.afterAllHooks) {
198
+ try {
199
+ await hook()
200
+ } catch (e) {
201
+ // Log but don't fail tests
202
+ console.error('afterAll hook failed:', e)
203
+ }
204
+ }
205
+
206
+ return this.buildResults(results, startTime)
207
+ }
208
+
209
+ /**
210
+ * Clear all registered tests and hooks
211
+ */
212
+ reset(): void {
213
+ this.tests = []
214
+ this.currentSuite = ''
215
+ this.beforeEachHooks = []
216
+ this.afterEachHooks = []
217
+ this.beforeAllHooks = []
218
+ this.afterAllHooks = []
219
+ }
220
+
221
+ private buildResults(results: TestResult[], startTime: number): TestResults {
222
+ return {
223
+ total: results.length,
224
+ passed: results.filter(r => r.passed && !r.skipped).length,
225
+ failed: results.filter(r => !r.passed).length,
226
+ skipped: results.filter(r => r.skipped).length,
227
+ tests: results,
228
+ duration: Date.now() - startTime
229
+ }
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Create a new test runner instance
235
+ */
236
+ export function createRunner(): TestRunner {
237
+ return new TestRunner()
238
+ }
package/src/types.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Types for ai-test
3
+ */
4
+
5
+ /**
6
+ * Test result for a single test
7
+ */
8
+ export interface TestResult {
9
+ name: string
10
+ passed: boolean
11
+ skipped?: boolean
12
+ error?: string
13
+ duration: number
14
+ }
15
+
16
+ /**
17
+ * Aggregated test results
18
+ */
19
+ export interface TestResults {
20
+ total: number
21
+ passed: number
22
+ failed: number
23
+ skipped: number
24
+ tests: TestResult[]
25
+ duration: number
26
+ }
27
+
28
+ /**
29
+ * Test function
30
+ */
31
+ export type TestFn = () => void | Promise<void>
32
+
33
+ /**
34
+ * Hook function (beforeEach, afterEach, etc.)
35
+ */
36
+ export type HookFn = () => void | Promise<void>
37
+
38
+ /**
39
+ * Registered test
40
+ */
41
+ export interface RegisteredTest {
42
+ name: string
43
+ fn: TestFn | null
44
+ hooks: {
45
+ before: HookFn[]
46
+ after: HookFn[]
47
+ }
48
+ skip?: boolean
49
+ only?: boolean
50
+ }
51
+
52
+ /**
53
+ * Test suite context
54
+ */
55
+ export interface SuiteContext {
56
+ name: string
57
+ beforeEach: HookFn[]
58
+ afterEach: HookFn[]
59
+ beforeAll: HookFn[]
60
+ afterAll: HookFn[]
61
+ }
62
+
63
+ /**
64
+ * Environment with test worker binding
65
+ */
66
+ export interface TestEnv {
67
+ TEST?: unknown
68
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Test Worker - provides test utilities via RPC
3
+ *
4
+ * This worker can be deployed to Cloudflare Workers or run locally via Miniflare.
5
+ * It exposes expect, should, assert, and a test runner via Workers RPC.
6
+ *
7
+ * Uses Cloudflare Workers RPC (WorkerEntrypoint, RpcTarget) for communication.
8
+ */
9
+
10
+ import { WorkerEntrypoint, RpcTarget } from 'cloudflare:workers'
11
+ import { Assertion, expect, should, assert } from './assertions.js'
12
+ import { TestRunner, createRunner } from './runner.js'
13
+
14
+ /**
15
+ * Core test service - extends RpcTarget so it can be passed over RPC
16
+ *
17
+ * Contains all test functionality: assertions (expect, should, assert)
18
+ * and test runner (describe, it, test, hooks)
19
+ */
20
+ export class TestServiceCore extends RpcTarget {
21
+ protected runner: TestRunner
22
+
23
+ constructor() {
24
+ super()
25
+ this.runner = createRunner()
26
+ }
27
+
28
+ expect(value: unknown, message?: string): Assertion {
29
+ return expect(value, message)
30
+ }
31
+
32
+ should(value: unknown): Assertion {
33
+ return should(value)
34
+ }
35
+
36
+ get assert() {
37
+ return assert
38
+ }
39
+
40
+ describe(name: string, fn: () => void): void {
41
+ this.runner.describe(name, fn)
42
+ }
43
+
44
+ it(name: string, fn: () => void | Promise<void>): void {
45
+ this.runner.it(name, fn)
46
+ }
47
+
48
+ test(name: string, fn: () => void | Promise<void>): void {
49
+ this.runner.test(name, fn)
50
+ }
51
+
52
+ skip(name: string, fn?: () => void | Promise<void>): void {
53
+ this.runner.skip(name, fn)
54
+ }
55
+
56
+ only(name: string, fn: () => void | Promise<void>): void {
57
+ this.runner.only(name, fn)
58
+ }
59
+
60
+ beforeEach(fn: () => void | Promise<void>): void {
61
+ this.runner.beforeEach(fn)
62
+ }
63
+
64
+ afterEach(fn: () => void | Promise<void>): void {
65
+ this.runner.afterEach(fn)
66
+ }
67
+
68
+ beforeAll(fn: () => void | Promise<void>): void {
69
+ this.runner.beforeAll(fn)
70
+ }
71
+
72
+ afterAll(fn: () => void | Promise<void>): void {
73
+ this.runner.afterAll(fn)
74
+ }
75
+
76
+ async run() {
77
+ return this.runner.run()
78
+ }
79
+
80
+ reset(): void {
81
+ this.runner.reset()
82
+ }
83
+
84
+ createRunner(): TestRunner {
85
+ return createRunner()
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Main test service exposed via RPC as WorkerEntrypoint
91
+ *
92
+ * Usage:
93
+ * const tests = await env.TEST.connect()
94
+ * tests.expect(1).to.equal(1)
95
+ * tests.describe('suite', () => { ... })
96
+ * const results = await tests.run()
97
+ */
98
+ export class TestService extends WorkerEntrypoint {
99
+ /**
100
+ * Get a test service instance - returns an RpcTarget that can be used directly
101
+ * This avoids boilerplate delegation and allows using `test` method name
102
+ */
103
+ connect(): TestServiceCore {
104
+ return new TestServiceCore()
105
+ }
106
+ }
107
+
108
+ // Export as default for WorkerEntrypoint pattern
109
+ export default TestService
110
+
111
+ // Export aliases
112
+ export { TestService as TestWorker }