@durable-streams/server-conformance-tests 0.1.0

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/README.md ADDED
@@ -0,0 +1,132 @@
1
+ # @durable-streams/server-conformance-tests
2
+
3
+ Protocol compliance test suite for Durable Streams server implementations.
4
+
5
+ This package provides a comprehensive test suite to verify that a server correctly implements the [Durable Streams protocol](../../PROTOCOL.md).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ npm install @durable-streams/server-conformance-tests
11
+ # or
12
+ pnpm add @durable-streams/server-conformance-tests
13
+ ```
14
+
15
+ ## CLI Usage
16
+
17
+ The easiest way to run conformance tests against your server:
18
+
19
+ ### Run Once (CI)
20
+
21
+ ```bash
22
+ npx @durable-streams/server-conformance-tests --run http://localhost:4437
23
+ ```
24
+
25
+ ### Watch Mode (Development)
26
+
27
+ Watch source files and automatically rerun tests when changes are detected:
28
+
29
+ ```bash
30
+ npx @durable-streams/server-conformance-tests --watch src http://localhost:4437
31
+
32
+ # Watch multiple directories
33
+ npx @durable-streams/server-conformance-tests --watch src lib http://localhost:4437
34
+ ```
35
+
36
+ ### CLI Options
37
+
38
+ ```
39
+ Usage:
40
+ npx @durable-streams/server-conformance-tests --run <url>
41
+ npx @durable-streams/server-conformance-tests --watch <path> [path...] <url>
42
+
43
+ Options:
44
+ --run Run tests once and exit (for CI)
45
+ --watch <paths> Watch source paths and rerun tests on changes (for development)
46
+ --help, -h Show help message
47
+
48
+ Arguments:
49
+ <url> Base URL of the Durable Streams server to test against
50
+ ```
51
+
52
+ ## Programmatic Usage
53
+
54
+ You can also run the conformance tests programmatically within your own test suite:
55
+
56
+ ```typescript
57
+ import { runConformanceTests } from "@durable-streams/server-conformance-tests"
58
+
59
+ // In your test file (e.g., with vitest)
60
+ describe("My Server Implementation", () => {
61
+ const config = { baseUrl: "" }
62
+
63
+ beforeAll(async () => {
64
+ // Start your server
65
+ const server = await startMyServer({ port: 0 })
66
+ config.baseUrl = server.url
67
+ })
68
+
69
+ afterAll(async () => {
70
+ await server.stop()
71
+ })
72
+
73
+ // Run all conformance tests
74
+ runConformanceTests(config)
75
+ })
76
+ ```
77
+
78
+ ## CI Integration
79
+
80
+ Add conformance tests to your CI pipeline:
81
+
82
+ ```yaml
83
+ # GitHub Actions example
84
+ jobs:
85
+ conformance:
86
+ runs-on: ubuntu-latest
87
+ steps:
88
+ - uses: actions/checkout@v4
89
+ - uses: actions/setup-node@v4
90
+ with:
91
+ node-version: "20"
92
+
93
+ - name: Install dependencies
94
+ run: npm install
95
+
96
+ - name: Start server
97
+ run: npm run start:server &
98
+
99
+ - name: Wait for server
100
+ run: npx wait-on http://localhost:4437
101
+
102
+ - name: Run conformance tests
103
+ run: npx @durable-streams/server-conformance-tests --run http://localhost:4437
104
+ ```
105
+
106
+ ## Test Coverage
107
+
108
+ The conformance test suite covers:
109
+
110
+ - **Basic Stream Operations** - Create, delete, idempotent operations
111
+ - **Append Operations** - String data, chunking, sequence ordering
112
+ - **Read Operations** - Empty/full streams, offset reads
113
+ - **Long-Poll Operations** - Data waiting, immediate returns
114
+ - **HTTP Protocol** - Headers, status codes, content negotiation
115
+ - **TTL and Expiry** - TTL/Expires-At handling
116
+ - **Case-Insensitivity** - Content-type, header casing
117
+ - **Content-Type Validation** - Match enforcement
118
+ - **HEAD Metadata** - Metadata-only responses
119
+ - **Offset Validation** - Malformed offsets, resumable reads
120
+ - **Protocol Edge Cases** - Empty bodies, binary data, monotonic progression
121
+ - **Byte-Exactness** - Data integrity guarantees
122
+ - **Caching and ETag** - ETag and 304 responses
123
+ - **Chunking and Large Payloads** - Pagination, large files
124
+ - **Property-Based Fuzzing** - Random append/read sequences
125
+ - **Malformed Input Fuzzing** - Security-focused tests
126
+ - **Read-Your-Writes Consistency** - Immediate visibility after writes
127
+ - **SSE Mode** - Server-sent events streaming
128
+ - **JSON Mode** - JSON serialization and batching
129
+
130
+ ## License
131
+
132
+ Apache 2.0
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+
3
+ /* eslint-disable stylistic/quotes */
4
+
5
+ /**
6
+ * Development wrapper that uses tsx to run the TypeScript source directly.
7
+ * This allows you to use `pnpm link --global` and see changes immediately
8
+ * without rebuilding.
9
+ */
10
+
11
+ import { spawn } from "node:child_process"
12
+ import { fileURLToPath } from "node:url"
13
+ import { dirname, join } from "node:path"
14
+
15
+ const __filename = fileURLToPath(import.meta.url)
16
+ const __dirname = dirname(__filename)
17
+
18
+ const srcPath = join(__dirname, "..", "src", "cli.ts")
19
+
20
+ // Run tsx with the source file
21
+ const child = spawn("tsx", [srcPath, ...process.argv.slice(2)], {
22
+ stdio: "inherit",
23
+ })
24
+
25
+ child.on("exit", (code) => {
26
+ process.exit(code ?? 0)
27
+ })
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.js ADDED
@@ -0,0 +1,221 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, watch } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ //#region src/cli.ts
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ function printUsage() {
10
+ console.log(`
11
+ Durable Streams Conformance Test Runner
12
+
13
+ Usage:
14
+ npx @durable-streams/server-conformance-tests --run <url>
15
+ npx @durable-streams/server-conformance-tests --watch <path> [path...] <url>
16
+
17
+ Options:
18
+ --run Run tests once and exit (for CI)
19
+ --watch <paths> Watch source paths and rerun tests on changes (for development)
20
+ --help, -h Show this help message
21
+
22
+ Arguments:
23
+ <url> Base URL of the Durable Streams server to test against
24
+
25
+ Examples:
26
+ # Run tests once in CI
27
+ npx @durable-streams/server-conformance-tests --run http://localhost:4473
28
+
29
+ # Watch src directory and rerun tests on changes
30
+ npx @durable-streams/server-conformance-tests --watch src http://localhost:4473
31
+
32
+ # Watch multiple directories
33
+ npx @durable-streams/server-conformance-tests --watch src lib http://localhost:4473
34
+ `);
35
+ }
36
+ function parseArgs(args) {
37
+ const result = {
38
+ mode: `run`,
39
+ watchPaths: [],
40
+ baseUrl: ``,
41
+ help: false
42
+ };
43
+ let i = 0;
44
+ while (i < args.length) {
45
+ const arg = args[i];
46
+ if (arg === `--help` || arg === `-h`) {
47
+ result.help = true;
48
+ return result;
49
+ }
50
+ if (arg === `--run`) {
51
+ result.mode = `run`;
52
+ i++;
53
+ continue;
54
+ }
55
+ if (arg === `--watch`) {
56
+ result.mode = `watch`;
57
+ i++;
58
+ while (i < args.length - 1) {
59
+ const next = args[i];
60
+ if (next.startsWith(`--`) || next.startsWith(`-`)) break;
61
+ result.watchPaths.push(next);
62
+ i++;
63
+ }
64
+ continue;
65
+ }
66
+ if (!arg.startsWith(`-`)) result.baseUrl = arg;
67
+ i++;
68
+ }
69
+ return result;
70
+ }
71
+ function validateArgs(args) {
72
+ if (!args.baseUrl) return `Error: Base URL is required`;
73
+ try {
74
+ new URL(args.baseUrl);
75
+ } catch {
76
+ return `Error: Invalid URL "${args.baseUrl}"`;
77
+ }
78
+ if (args.mode === `watch` && args.watchPaths.length === 0) return `Error: --watch requires at least one path to watch`;
79
+ return null;
80
+ }
81
+ function getTestRunnerPath() {
82
+ const runnerInDist = join(__dirname, `test-runner.js`);
83
+ const runnerInSrc = join(__dirname, `test-runner.ts`);
84
+ if (existsSync(runnerInDist)) return runnerInDist;
85
+ return runnerInSrc;
86
+ }
87
+ function runTests(baseUrl) {
88
+ return new Promise((resolvePromise) => {
89
+ const runnerPath = getTestRunnerPath();
90
+ const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`);
91
+ const vitestBinAlt = join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
92
+ let vitestPath = `vitest`;
93
+ if (existsSync(vitestBin)) vitestPath = vitestBin;
94
+ else if (existsSync(vitestBinAlt)) vitestPath = vitestBinAlt;
95
+ const args = [
96
+ `run`,
97
+ runnerPath,
98
+ `--no-coverage`,
99
+ `--reporter=default`,
100
+ `--passWithNoTests=false`
101
+ ];
102
+ const child = spawn(vitestPath, args, {
103
+ stdio: `inherit`,
104
+ env: {
105
+ ...process.env,
106
+ CONFORMANCE_TEST_URL: baseUrl,
107
+ FORCE_COLOR: `1`
108
+ },
109
+ shell: true
110
+ });
111
+ child.on(`close`, (code) => {
112
+ resolvePromise(code ?? 1);
113
+ });
114
+ child.on(`error`, (err) => {
115
+ console.error(`Failed to run tests: ${err.message}`);
116
+ resolvePromise(1);
117
+ });
118
+ });
119
+ }
120
+ async function runOnce(baseUrl) {
121
+ console.log(`Running conformance tests against ${baseUrl}\n`);
122
+ const exitCode = await runTests(baseUrl);
123
+ process.exit(exitCode);
124
+ }
125
+ async function runWatch(baseUrl, watchPaths) {
126
+ let runningProcess = null;
127
+ let debounceTimer = null;
128
+ const DEBOUNCE_MS = 300;
129
+ const spawnTests = () => {
130
+ const runnerPath = getTestRunnerPath();
131
+ const vitestBin = join(__dirname, `..`, `node_modules`, `.bin`, `vitest`);
132
+ const vitestBinAlt = join(__dirname, `..`, `..`, `..`, `node_modules`, `.bin`, `vitest`);
133
+ let vitestPath = `vitest`;
134
+ if (existsSync(vitestBin)) vitestPath = vitestBin;
135
+ else if (existsSync(vitestBinAlt)) vitestPath = vitestBinAlt;
136
+ const args = [
137
+ `run`,
138
+ runnerPath,
139
+ `--no-coverage`,
140
+ `--reporter=default`,
141
+ `--passWithNoTests=false`
142
+ ];
143
+ return spawn(vitestPath, args, {
144
+ stdio: `inherit`,
145
+ env: {
146
+ ...process.env,
147
+ CONFORMANCE_TEST_URL: baseUrl,
148
+ FORCE_COLOR: `1`
149
+ },
150
+ shell: true
151
+ });
152
+ };
153
+ const runTestsDebounced = () => {
154
+ if (debounceTimer) clearTimeout(debounceTimer);
155
+ debounceTimer = setTimeout(() => {
156
+ if (runningProcess) {
157
+ runningProcess.kill(`SIGTERM`);
158
+ runningProcess = null;
159
+ }
160
+ console.clear();
161
+ console.log(`Running conformance tests against ${baseUrl}\n`);
162
+ runningProcess = spawnTests();
163
+ runningProcess.on(`close`, (code) => {
164
+ if (code === 0) console.log(`\nAll tests passed`);
165
+ else console.log(`\nTests failed (exit code: ${code})`);
166
+ console.log(`\nWatching for changes in: ${watchPaths.join(`, `)}`);
167
+ console.log(`Press Ctrl+C to exit\n`);
168
+ runningProcess = null;
169
+ });
170
+ }, DEBOUNCE_MS);
171
+ };
172
+ const watchers = [];
173
+ for (const watchPath of watchPaths) {
174
+ const absPath = resolve(process.cwd(), watchPath);
175
+ try {
176
+ const watcher = watch(absPath, { recursive: true }, (eventType, filename) => {
177
+ if (filename && !filename.includes(`node_modules`)) {
178
+ console.log(`\nChange detected: ${filename}`);
179
+ runTestsDebounced();
180
+ }
181
+ });
182
+ watchers.push(watcher);
183
+ console.log(`Watching: ${absPath}`);
184
+ } catch (err) {
185
+ console.error(`Warning: Could not watch "${watchPath}": ${err.message}`);
186
+ }
187
+ }
188
+ if (watchers.length === 0) {
189
+ console.error(`Error: No valid paths to watch`);
190
+ process.exit(1);
191
+ }
192
+ process.on(`SIGINT`, () => {
193
+ console.log(`\n\nStopping watch mode...`);
194
+ watchers.forEach((w) => w.close());
195
+ if (runningProcess) runningProcess.kill(`SIGTERM`);
196
+ process.exit(0);
197
+ });
198
+ runTestsDebounced();
199
+ await new Promise(() => {});
200
+ }
201
+ async function main() {
202
+ const args = parseArgs(process.argv.slice(2));
203
+ if (args.help) {
204
+ printUsage();
205
+ process.exit(0);
206
+ }
207
+ const error = validateArgs(args);
208
+ if (error) {
209
+ console.error(error);
210
+ console.error(`\nRun with --help for usage information`);
211
+ process.exit(1);
212
+ }
213
+ if (args.mode === `watch`) await runWatch(args.baseUrl, args.watchPaths);
214
+ else await runOnce(args.baseUrl);
215
+ }
216
+ main().catch((err) => {
217
+ console.error(`Fatal error: ${err.message}`);
218
+ process.exit(1);
219
+ });
220
+
221
+ //#endregion
@@ -0,0 +1,10 @@
1
+ //#region src/index.d.ts
2
+ interface ConformanceTestOptions {
3
+ /** Base URL of the server to test */
4
+ baseUrl: string;
5
+ }
6
+ /**
7
+ * Run the full conformance test suite against a server
8
+ */
9
+ declare function runConformanceTests(options: ConformanceTestOptions): void; //#endregion
10
+ export { ConformanceTestOptions, runConformanceTests };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { runConformanceTests } from "./src-DRIMnUPk.js";
2
+
3
+ export { runConformanceTests };