ai-tests 2.0.2 → 2.1.3

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/cli.js ADDED
@@ -0,0 +1,76 @@
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
+ import { spawn } from 'node:child_process';
13
+ import { fileURLToPath } from 'node:url';
14
+ import { dirname, join } from 'node:path';
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const packageRoot = join(__dirname, '..');
17
+ const commands = {
18
+ deploy: {
19
+ description: 'Deploy ai-tests worker to Cloudflare',
20
+ args: ['wrangler', 'deploy']
21
+ },
22
+ dev: {
23
+ description: 'Start local development server',
24
+ args: ['wrangler', 'dev']
25
+ }
26
+ };
27
+ function showHelp() {
28
+ console.log(`
29
+ ai-tests - Test utilities worker for Cloudflare Workers
30
+
31
+ Usage:
32
+ npx ai-tests <command>
33
+
34
+ Commands:
35
+ deploy Deploy to Cloudflare Workers
36
+ dev Start local development server
37
+ help Show this help message
38
+
39
+ Examples:
40
+ npx ai-tests deploy
41
+ npx ai-tests dev
42
+
43
+ After deploying, bind the worker to your sandbox worker:
44
+ # In your wrangler.toml:
45
+ [[services]]
46
+ binding = "TEST"
47
+ service = "ai-tests"
48
+ `);
49
+ }
50
+ async function run() {
51
+ const args = process.argv.slice(2);
52
+ const command = args[0];
53
+ if (!command || command === 'help' || command === '--help' || command === '-h') {
54
+ showHelp();
55
+ process.exit(0);
56
+ }
57
+ const cmd = commands[command];
58
+ if (!cmd) {
59
+ console.error(`Unknown command: ${command}`);
60
+ showHelp();
61
+ process.exit(1);
62
+ }
63
+ console.log(`Running: ${cmd.args.join(' ')}`);
64
+ const child = spawn('npx', cmd.args, {
65
+ cwd: packageRoot,
66
+ stdio: 'inherit',
67
+ shell: true
68
+ });
69
+ child.on('close', (code) => {
70
+ process.exit(code ?? 0);
71
+ });
72
+ }
73
+ run().catch((err) => {
74
+ console.error(err);
75
+ process.exit(1);
76
+ });
package/src/index.js ADDED
@@ -0,0 +1,18 @@
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
+ // Export assertion utilities
12
+ export { Assertion, expect, should, assert } from './assertions.js';
13
+ // Export test runner
14
+ export { TestRunner, createRunner } from './runner.js';
15
+ // Export worker service (WorkerEntrypoint and RpcTarget)
16
+ export { TestService, TestService as TestWorker, TestServiceCore } from './worker.js';
17
+ // Export the default WorkerEntrypoint class
18
+ export { default } from './worker.js';
package/src/local.js ADDED
@@ -0,0 +1,62 @@
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
+ import { TestServiceCore } from './worker.js';
7
+ let miniflareInstance = null;
8
+ let localService = null;
9
+ /**
10
+ * Get a local TestServiceCore instance
11
+ *
12
+ * For local development, this creates a direct instance.
13
+ * The RPC serialization happens when called via worker bindings.
14
+ */
15
+ export function getLocalTestService() {
16
+ if (!localService) {
17
+ localService = new TestServiceCore();
18
+ }
19
+ return localService;
20
+ }
21
+ /**
22
+ * Start a Miniflare instance running the test worker
23
+ *
24
+ * This is useful for testing the RPC interface locally.
25
+ */
26
+ export async function startTestWorker(options) {
27
+ const { Miniflare } = await import('miniflare');
28
+ // Build worker script inline
29
+ const workerScript = `
30
+ import { TestService } from './index.js';
31
+ export { TestService };
32
+ export default {
33
+ async fetch(request) {
34
+ return new Response('ai-tests worker running');
35
+ }
36
+ };
37
+ `;
38
+ miniflareInstance = new Miniflare({
39
+ modules: true,
40
+ script: workerScript,
41
+ port: options?.port,
42
+ compatibilityDate: '2024-12-01',
43
+ });
44
+ const url = await miniflareInstance.ready;
45
+ return {
46
+ url: url.toString(),
47
+ stop: async () => {
48
+ if (miniflareInstance) {
49
+ await miniflareInstance.dispose();
50
+ miniflareInstance = null;
51
+ }
52
+ }
53
+ };
54
+ }
55
+ /**
56
+ * Create a test service that can be used as a service binding
57
+ *
58
+ * For use in Miniflare configurations when testing sandbox workers.
59
+ */
60
+ export function createTestServiceBinding() {
61
+ return new TestServiceCore();
62
+ }
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 };