@durable-streams/client-conformance-tests 0.1.5 → 0.1.6

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.
@@ -471,12 +471,13 @@ async function handleBenchmark(command) {
471
471
  const readPromise = (async () => {
472
472
  const res = await ds.stream({ live: operation.live ?? `long-poll` });
473
473
  return new Promise((resolve) => {
474
- const unsubscribe = res.subscribeBytes(async (chunk) => {
474
+ const unsubscribe = res.subscribeBytes((chunk) => {
475
475
  if (chunk.data.length > 0) {
476
476
  unsubscribe();
477
477
  res.cancel();
478
478
  resolve(chunk.data);
479
479
  }
480
+ return Promise.resolve();
480
481
  });
481
482
  });
482
483
  })();
@@ -469,12 +469,13 @@ async function handleBenchmark(command) {
469
469
  const readPromise = (async () => {
470
470
  const res = await ds.stream({ live: operation.live ?? `long-poll` });
471
471
  return new Promise((resolve) => {
472
- const unsubscribe = res.subscribeBytes(async (chunk) => {
472
+ const unsubscribe = res.subscribeBytes((chunk) => {
473
473
  if (chunk.data.length > 0) {
474
474
  unsubscribe();
475
475
  res.cancel();
476
476
  resolve(chunk.data);
477
477
  }
478
+ return Promise.resolve();
478
479
  });
479
480
  });
480
481
  })();
@@ -352,14 +352,30 @@ async function executeOperation(op, ctx) {
352
352
  path,
353
353
  status: op.status,
354
354
  count: op.count ?? 1,
355
- retryAfter: op.retryAfter
355
+ retryAfter: op.retryAfter,
356
+ delayMs: op.delayMs,
357
+ dropConnection: op.dropConnection,
358
+ truncateBodyBytes: op.truncateBodyBytes,
359
+ probability: op.probability,
360
+ method: op.method,
361
+ corruptBody: op.corruptBody,
362
+ jitterMs: op.jitterMs
356
363
  })
357
364
  });
358
- if (verbose) console.log(` inject-error ${path} ${op.status}x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`);
359
- if (!response.ok) return { error: `Failed to inject error: ${response.status}` };
365
+ const faultTypes = [];
366
+ if (op.status != null) faultTypes.push(`status=${op.status}`);
367
+ if (op.delayMs != null) faultTypes.push(`delay=${op.delayMs}ms`);
368
+ if (op.jitterMs != null) faultTypes.push(`jitter=${op.jitterMs}ms`);
369
+ if (op.dropConnection) faultTypes.push(`dropConnection`);
370
+ if (op.truncateBodyBytes != null) faultTypes.push(`truncate=${op.truncateBodyBytes}b`);
371
+ if (op.corruptBody) faultTypes.push(`corrupt`);
372
+ if (op.probability != null) faultTypes.push(`p=${op.probability}`);
373
+ const faultDesc = faultTypes.join(`,`) || `unknown`;
374
+ if (verbose) console.log(` inject-error ${path} [${faultDesc}]x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`);
375
+ if (!response.ok) return { error: `Failed to inject fault: ${response.status}` };
360
376
  return {};
361
377
  } catch (err) {
362
- return { error: `Failed to inject error: ${err instanceof Error ? err.message : String(err)}` };
378
+ return { error: `Failed to inject fault: ${err instanceof Error ? err.message : String(err)}` };
363
379
  }
364
380
  }
365
381
  case `clear-errors`: try {
@@ -350,14 +350,30 @@ async function executeOperation(op, ctx) {
350
350
  path: path$1,
351
351
  status: op.status,
352
352
  count: op.count ?? 1,
353
- retryAfter: op.retryAfter
353
+ retryAfter: op.retryAfter,
354
+ delayMs: op.delayMs,
355
+ dropConnection: op.dropConnection,
356
+ truncateBodyBytes: op.truncateBodyBytes,
357
+ probability: op.probability,
358
+ method: op.method,
359
+ corruptBody: op.corruptBody,
360
+ jitterMs: op.jitterMs
354
361
  })
355
362
  });
356
- if (verbose) console.log(` inject-error ${path$1} ${op.status}x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`);
357
- if (!response.ok) return { error: `Failed to inject error: ${response.status}` };
363
+ const faultTypes = [];
364
+ if (op.status != null) faultTypes.push(`status=${op.status}`);
365
+ if (op.delayMs != null) faultTypes.push(`delay=${op.delayMs}ms`);
366
+ if (op.jitterMs != null) faultTypes.push(`jitter=${op.jitterMs}ms`);
367
+ if (op.dropConnection) faultTypes.push(`dropConnection`);
368
+ if (op.truncateBodyBytes != null) faultTypes.push(`truncate=${op.truncateBodyBytes}b`);
369
+ if (op.corruptBody) faultTypes.push(`corrupt`);
370
+ if (op.probability != null) faultTypes.push(`p=${op.probability}`);
371
+ const faultDesc = faultTypes.join(`,`) || `unknown`;
372
+ if (verbose) console.log(` inject-error ${path$1} [${faultDesc}]x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`);
373
+ if (!response.ok) return { error: `Failed to inject fault: ${response.status}` };
358
374
  return {};
359
375
  } catch (err) {
360
- return { error: `Failed to inject error: ${err instanceof Error ? err.message : String(err)}` };
376
+ return { error: `Failed to inject fault: ${err instanceof Error ? err.message : String(err)}` };
361
377
  }
362
378
  }
363
379
  case `clear-errors`: try {
package/dist/cli.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
  require('./protocol-XeAOKBD-.cjs');
4
- const require_benchmark_runner = require('./benchmark-runner-CLAR9oLd.cjs');
4
+ const require_benchmark_runner = require('./benchmark-runner-BlKqhoXE.cjs');
5
5
 
6
6
  //#region src/cli.ts
7
7
  const HELP = `
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import "./protocol-qb83AeUH.js";
3
- import { runBenchmarks, runConformanceTests } from "./benchmark-runner-C_Yghc8f.js";
3
+ import { runBenchmarks, runConformanceTests } from "./benchmark-runner-D-YSAvRy.js";
4
4
 
5
5
  //#region src/cli.ts
6
6
  const HELP = `
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  const require_protocol = require('./protocol-XeAOKBD-.cjs');
2
- const require_benchmark_runner = require('./benchmark-runner-CLAR9oLd.cjs');
2
+ const require_benchmark_runner = require('./benchmark-runner-BlKqhoXE.cjs');
3
3
 
4
4
  exports.ErrorCodes = require_protocol.ErrorCodes
5
5
  exports.allScenarios = require_benchmark_runner.allScenarios
package/dist/index.d.cts CHANGED
@@ -212,19 +212,34 @@ interface AwaitOperation {
212
212
  expect?: ReadExpectation;
213
213
  }
214
214
  /**
215
- * Inject an error to be returned on the next N requests to a path.
215
+ * Inject a fault to be triggered on the next N requests to a path.
216
216
  * Used for testing retry/resilience behavior.
217
+ * Supports various fault types: errors, delays, connection drops, body corruption, etc.
217
218
  */
218
219
  interface InjectErrorOperation {
219
220
  action: `inject-error`;
220
- /** Stream path to inject error for */
221
+ /** Stream path to inject fault for */
221
222
  path: string;
222
- /** HTTP status code to return */
223
- status: number;
224
- /** Number of times to return this error (default: 1) */
223
+ /** HTTP status code to return (if set, returns error response) */
224
+ status?: number;
225
+ /** Number of times to trigger this fault (default: 1) */
225
226
  count?: number;
226
227
  /** Optional Retry-After header value (seconds) */
227
228
  retryAfter?: number;
229
+ /** Delay in milliseconds before responding */
230
+ delayMs?: number;
231
+ /** Drop the connection after sending headers (simulates network failure) */
232
+ dropConnection?: boolean;
233
+ /** Truncate response body to this many bytes */
234
+ truncateBodyBytes?: number;
235
+ /** Probability of triggering fault (0-1, default 1.0 = always) */
236
+ probability?: number;
237
+ /** Only match specific HTTP method (GET, POST, PUT, DELETE) */
238
+ method?: string;
239
+ /** Corrupt the response body by flipping random bits */
240
+ corruptBody?: boolean;
241
+ /** Add jitter to delay (random 0-jitterMs added to delayMs) */
242
+ jitterMs?: number;
228
243
  }
229
244
  /**
230
245
  * Clear all injected errors.
package/dist/index.d.ts CHANGED
@@ -212,19 +212,34 @@ interface AwaitOperation {
212
212
  expect?: ReadExpectation;
213
213
  }
214
214
  /**
215
- * Inject an error to be returned on the next N requests to a path.
215
+ * Inject a fault to be triggered on the next N requests to a path.
216
216
  * Used for testing retry/resilience behavior.
217
+ * Supports various fault types: errors, delays, connection drops, body corruption, etc.
217
218
  */
218
219
  interface InjectErrorOperation {
219
220
  action: `inject-error`;
220
- /** Stream path to inject error for */
221
+ /** Stream path to inject fault for */
221
222
  path: string;
222
- /** HTTP status code to return */
223
- status: number;
224
- /** Number of times to return this error (default: 1) */
223
+ /** HTTP status code to return (if set, returns error response) */
224
+ status?: number;
225
+ /** Number of times to trigger this fault (default: 1) */
225
226
  count?: number;
226
227
  /** Optional Retry-After header value (seconds) */
227
228
  retryAfter?: number;
229
+ /** Delay in milliseconds before responding */
230
+ delayMs?: number;
231
+ /** Drop the connection after sending headers (simulates network failure) */
232
+ dropConnection?: boolean;
233
+ /** Truncate response body to this many bytes */
234
+ truncateBodyBytes?: number;
235
+ /** Probability of triggering fault (0-1, default 1.0 = always) */
236
+ probability?: number;
237
+ /** Only match specific HTTP method (GET, POST, PUT, DELETE) */
238
+ method?: string;
239
+ /** Corrupt the response body by flipping random bits */
240
+ corruptBody?: boolean;
241
+ /** Add jitter to delay (random 0-jitterMs added to delayMs) */
242
+ jitterMs?: number;
228
243
  }
229
244
  /**
230
245
  * Clear all injected errors.
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { ErrorCodes, calculateStats, decodeBase64, encodeBase64, formatStats, parseCommand, parseResult, serializeCommand, serializeResult } from "./protocol-qb83AeUH.js";
2
- import { allScenarios, countTests, filterByCategory, getScenarioById, getScenariosByCategory, loadEmbeddedTestSuites, loadTestSuites, runBenchmarks, runConformanceTests, scenariosByCategory } from "./benchmark-runner-C_Yghc8f.js";
2
+ import { allScenarios, countTests, filterByCategory, getScenarioById, getScenariosByCategory, loadEmbeddedTestSuites, loadTestSuites, runBenchmarks, runConformanceTests, scenariosByCategory } from "./benchmark-runner-D-YSAvRy.js";
3
3
 
4
4
  export { ErrorCodes, allScenarios, calculateStats, countTests, decodeBase64, encodeBase64, filterByCategory, formatStats, getScenarioById, getScenariosByCategory, loadEmbeddedTestSuites, loadTestSuites, parseCommand, parseResult, runBenchmarks, runConformanceTests, scenariosByCategory, serializeCommand, serializeResult };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@durable-streams/client-conformance-tests",
3
3
  "description": "Conformance test suite for Durable Streams client implementations (producer and consumer)",
4
- "version": "0.1.5",
4
+ "version": "0.1.6",
5
5
  "author": "Durable Stream contributors",
6
6
  "bin": {
7
7
  "client-conformance-tests": "./dist/cli.js",
@@ -14,8 +14,8 @@
14
14
  "fast-check": "^4.4.0",
15
15
  "tsx": "^4.19.2",
16
16
  "yaml": "^2.7.1",
17
- "@durable-streams/client": "0.1.2",
18
- "@durable-streams/server": "0.1.3"
17
+ "@durable-streams/client": "0.1.3",
18
+ "@durable-streams/server": "0.1.4"
19
19
  },
20
20
  "devDependencies": {
21
21
  "tsdown": "^0.9.0",
@@ -702,12 +702,13 @@ async function handleBenchmark(command: BenchmarkCommand): Promise<TestResult> {
702
702
 
703
703
  // Wait for data
704
704
  return new Promise<Uint8Array>((resolve) => {
705
- const unsubscribe = res.subscribeBytes(async (chunk) => {
705
+ const unsubscribe = res.subscribeBytes((chunk) => {
706
706
  if (chunk.data.length > 0) {
707
707
  unsubscribe()
708
708
  res.cancel()
709
709
  resolve(chunk.data)
710
710
  }
711
+ return Promise.resolve()
711
712
  })
712
713
  })
713
714
  })()
package/src/runner.ts CHANGED
@@ -599,7 +599,7 @@ async function executeOperation(
599
599
  }
600
600
 
601
601
  case `inject-error`: {
602
- // Inject an error via the test server's control endpoint
602
+ // Inject a fault via the test server's control endpoint
603
603
  const path = resolveVariables(op.path, variables)
604
604
 
605
605
  try {
@@ -611,23 +611,43 @@ async function executeOperation(
611
611
  status: op.status,
612
612
  count: op.count ?? 1,
613
613
  retryAfter: op.retryAfter,
614
+ // New fault injection parameters
615
+ delayMs: op.delayMs,
616
+ dropConnection: op.dropConnection,
617
+ truncateBodyBytes: op.truncateBodyBytes,
618
+ probability: op.probability,
619
+ method: op.method,
620
+ corruptBody: op.corruptBody,
621
+ jitterMs: op.jitterMs,
614
622
  }),
615
623
  })
616
624
 
625
+ // Build descriptive log message
626
+ const faultTypes = []
627
+ if (op.status != null) faultTypes.push(`status=${op.status}`)
628
+ if (op.delayMs != null) faultTypes.push(`delay=${op.delayMs}ms`)
629
+ if (op.jitterMs != null) faultTypes.push(`jitter=${op.jitterMs}ms`)
630
+ if (op.dropConnection) faultTypes.push(`dropConnection`)
631
+ if (op.truncateBodyBytes != null)
632
+ faultTypes.push(`truncate=${op.truncateBodyBytes}b`)
633
+ if (op.corruptBody) faultTypes.push(`corrupt`)
634
+ if (op.probability != null) faultTypes.push(`p=${op.probability}`)
635
+ const faultDesc = faultTypes.join(`,`) || `unknown`
636
+
617
637
  if (verbose) {
618
638
  console.log(
619
- ` inject-error ${path} ${op.status}x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`
639
+ ` inject-error ${path} [${faultDesc}]x${op.count ?? 1}: ${response.ok ? `ok` : `failed`}`
620
640
  )
621
641
  }
622
642
 
623
643
  if (!response.ok) {
624
- return { error: `Failed to inject error: ${response.status}` }
644
+ return { error: `Failed to inject fault: ${response.status}` }
625
645
  }
626
646
 
627
647
  return {}
628
648
  } catch (err) {
629
649
  return {
630
- error: `Failed to inject error: ${err instanceof Error ? err.message : String(err)}`,
650
+ error: `Failed to inject fault: ${err instanceof Error ? err.message : String(err)}`,
631
651
  }
632
652
  }
633
653
  }
package/src/test-cases.ts CHANGED
@@ -243,19 +243,34 @@ export interface AwaitOperation {
243
243
  }
244
244
 
245
245
  /**
246
- * Inject an error to be returned on the next N requests to a path.
246
+ * Inject a fault to be triggered on the next N requests to a path.
247
247
  * Used for testing retry/resilience behavior.
248
+ * Supports various fault types: errors, delays, connection drops, body corruption, etc.
248
249
  */
249
250
  export interface InjectErrorOperation {
250
251
  action: `inject-error`
251
- /** Stream path to inject error for */
252
+ /** Stream path to inject fault for */
252
253
  path: string
253
- /** HTTP status code to return */
254
- status: number
255
- /** Number of times to return this error (default: 1) */
254
+ /** HTTP status code to return (if set, returns error response) */
255
+ status?: number
256
+ /** Number of times to trigger this fault (default: 1) */
256
257
  count?: number
257
258
  /** Optional Retry-After header value (seconds) */
258
259
  retryAfter?: number
260
+ /** Delay in milliseconds before responding */
261
+ delayMs?: number
262
+ /** Drop the connection after sending headers (simulates network failure) */
263
+ dropConnection?: boolean
264
+ /** Truncate response body to this many bytes */
265
+ truncateBodyBytes?: number
266
+ /** Probability of triggering fault (0-1, default 1.0 = always) */
267
+ probability?: number
268
+ /** Only match specific HTTP method (GET, POST, PUT, DELETE) */
269
+ method?: string
270
+ /** Corrupt the response body by flipping random bits */
271
+ corruptBody?: boolean
272
+ /** Add jitter to delay (random 0-jitterMs added to delayMs) */
273
+ jitterMs?: number
259
274
  }
260
275
 
261
276
  /**
@@ -0,0 +1,202 @@
1
+ id: consumer-fault-injection
2
+ name: Advanced Fault Injection
3
+ description: Tests for client resilience against various network and server faults
4
+ category: consumer
5
+ tags:
6
+ - fault-injection
7
+ - resilience
8
+ - chaos
9
+ - advanced
10
+
11
+ tests:
12
+ - id: delay-recovery
13
+ name: Client handles delayed responses
14
+ description: Client should successfully read data even with server delays
15
+ setup:
16
+ - action: create
17
+ as: streamPath
18
+ - action: append
19
+ path: ${streamPath}
20
+ data: "delayed-data"
21
+ operations:
22
+ # Inject a 500ms delay
23
+ - action: inject-error
24
+ path: ${streamPath}
25
+ delayMs: 500
26
+ count: 1
27
+ # Client should wait and succeed
28
+ - action: read
29
+ path: ${streamPath}
30
+ expect:
31
+ data: "delayed-data"
32
+ cleanup:
33
+ - action: clear-errors
34
+
35
+ - id: delay-with-jitter
36
+ name: Client handles delayed responses with jitter
37
+ description: Client should handle variable delays (delay + random jitter)
38
+ setup:
39
+ - action: create
40
+ as: streamPath
41
+ - action: append
42
+ path: ${streamPath}
43
+ data: "jittery-data"
44
+ operations:
45
+ # Inject delay with jitter (300-600ms total)
46
+ - action: inject-error
47
+ path: ${streamPath}
48
+ delayMs: 300
49
+ jitterMs: 300
50
+ count: 1
51
+ - action: read
52
+ path: ${streamPath}
53
+ expect:
54
+ data: "jittery-data"
55
+ cleanup:
56
+ - action: clear-errors
57
+
58
+ - id: connection-drop-recovery
59
+ name: Client recovers from dropped connection
60
+ description: Client should retry when connection is dropped mid-request
61
+ setup:
62
+ - action: create
63
+ as: streamPath
64
+ - action: append
65
+ path: ${streamPath}
66
+ data: "persistent-data"
67
+ operations:
68
+ # Drop connection once
69
+ - action: inject-error
70
+ path: ${streamPath}
71
+ dropConnection: true
72
+ count: 1
73
+ # Client should retry and eventually succeed
74
+ - action: read
75
+ path: ${streamPath}
76
+ expect:
77
+ data: "persistent-data"
78
+ cleanup:
79
+ - action: clear-errors
80
+
81
+ - id: multiple-connection-drops
82
+ name: Client recovers from multiple dropped connections
83
+ description: Client should retry through multiple connection failures
84
+ setup:
85
+ - action: create
86
+ as: streamPath
87
+ - action: append
88
+ path: ${streamPath}
89
+ data: "resilient-data"
90
+ operations:
91
+ # Drop connection twice
92
+ - action: inject-error
93
+ path: ${streamPath}
94
+ dropConnection: true
95
+ count: 2
96
+ # Client should retry multiple times and succeed
97
+ - action: read
98
+ path: ${streamPath}
99
+ expect:
100
+ data: "resilient-data"
101
+ cleanup:
102
+ - action: clear-errors
103
+
104
+ - id: method-specific-fault
105
+ name: Fault only affects specific HTTP method
106
+ description: Fault should only trigger for specified method
107
+ setup:
108
+ - action: create
109
+ as: streamPath
110
+ operations:
111
+ # Inject fault only for POST (append)
112
+ - action: inject-error
113
+ path: ${streamPath}
114
+ status: 503
115
+ method: POST
116
+ count: 1
117
+ # GET should work fine (no fault)
118
+ - action: read
119
+ path: ${streamPath}
120
+ expect:
121
+ data: ""
122
+ # POST (append) should fail first then succeed on retry
123
+ - action: append
124
+ path: ${streamPath}
125
+ data: "method-filtered"
126
+ # Verify data was appended
127
+ - action: read
128
+ path: ${streamPath}
129
+ expect:
130
+ data: "method-filtered"
131
+ cleanup:
132
+ - action: clear-errors
133
+
134
+ - id: delay-then-error
135
+ name: Delay followed by error
136
+ description: Combined delay and error response
137
+ setup:
138
+ - action: create
139
+ as: streamPath
140
+ - action: append
141
+ path: ${streamPath}
142
+ data: "combined-fault-data"
143
+ operations:
144
+ # Delay 200ms then return 503
145
+ - action: inject-error
146
+ path: ${streamPath}
147
+ delayMs: 200
148
+ status: 503
149
+ count: 1
150
+ # Client should wait, get error, retry, and succeed
151
+ - action: read
152
+ path: ${streamPath}
153
+ expect:
154
+ data: "combined-fault-data"
155
+ cleanup:
156
+ - action: clear-errors
157
+
158
+ - id: append-with-delay
159
+ name: Append succeeds with server delay
160
+ description: Append should complete successfully even with server-side delay
161
+ setup:
162
+ - action: create
163
+ as: streamPath
164
+ operations:
165
+ # Add 500ms delay on first append
166
+ - action: inject-error
167
+ path: ${streamPath}
168
+ delayMs: 500
169
+ count: 1
170
+ # Append should wait and succeed
171
+ - action: append
172
+ path: ${streamPath}
173
+ data: "delayed-append"
174
+ # Verify data was appended
175
+ - action: read
176
+ path: ${streamPath}
177
+ expect:
178
+ data: "delayed-append"
179
+ cleanup:
180
+ - action: clear-errors
181
+
182
+ - id: delay-under-timeout
183
+ name: Client succeeds with delay under timeout
184
+ description: Client should succeed when delay is less than client timeout
185
+ setup:
186
+ - action: create
187
+ as: streamPath
188
+ - action: append
189
+ path: ${streamPath}
190
+ data: "within-timeout"
191
+ operations:
192
+ # 1 second delay (should be under most client timeouts)
193
+ - action: inject-error
194
+ path: ${streamPath}
195
+ delayMs: 1000
196
+ count: 1
197
+ - action: read
198
+ path: ${streamPath}
199
+ expect:
200
+ data: "within-timeout"
201
+ cleanup:
202
+ - action: clear-errors