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