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/.turbo/turbo-build.log +4 -5
- package/.turbo/turbo-test.log +8 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +21 -0
- package/dist/assertions.d.ts +79 -7
- package/dist/assertions.d.ts.map +1 -1
- package/dist/assertions.js +80 -14
- package/dist/assertions.js.map +1 -1
- package/package.json +13 -14
- package/src/assertions.js +383 -0
- package/src/assertions.ts +143 -32
- package/src/cli.js +76 -0
- package/src/index.js +18 -0
- package/src/local.js +62 -0
- package/src/runner.js +214 -0
- package/src/types.js +4 -0
- package/src/worker.js +91 -0
- package/test/assertions.test.js +493 -0
- package/test/index.test.js +42 -0
- package/test/local.test.js +27 -0
- package/test/runner.test.js +315 -0
- package/test/type-safety-assertions.test.ts +201 -0
- package/test/worker.test.js +162 -0
- package/vitest.config.js +10 -0
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
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 };
|