@durable-streams/client-conformance-tests 0.1.4 → 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.
- package/dist/adapters/typescript-adapter.cjs +2 -1
- package/dist/adapters/typescript-adapter.js +2 -1
- package/dist/{benchmark-runner-CLAR9oLd.cjs → benchmark-runner-BlKqhoXE.cjs} +20 -4
- package/dist/{benchmark-runner-C_Yghc8f.js → benchmark-runner-D-YSAvRy.js} +20 -4
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +20 -5
- package/dist/index.d.ts +20 -5
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/adapters/typescript-adapter.ts +2 -1
- package/src/runner.ts +24 -4
- package/src/test-cases.ts +20 -5
- package/test-cases/consumer/fault-injection.yaml +202 -0
- package/test-cases/consumer/read-sse.yaml +87 -0
|
@@ -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(
|
|
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(
|
|
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
|
-
|
|
359
|
-
if (
|
|
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
|
|
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
|
-
|
|
357
|
-
if (
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
221
|
+
/** Stream path to inject fault for */
|
|
221
222
|
path: string;
|
|
222
|
-
/** HTTP status code to return */
|
|
223
|
-
status
|
|
224
|
-
/** Number of times to
|
|
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
|
|
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
|
|
221
|
+
/** Stream path to inject fault for */
|
|
221
222
|
path: string;
|
|
222
|
-
/** HTTP status code to return */
|
|
223
|
-
status
|
|
224
|
-
/** Number of times to
|
|
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-
|
|
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.
|
|
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.
|
|
18
|
-
"@durable-streams/server": "0.1.
|
|
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(
|
|
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
|
|
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} ${
|
|
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
|
|
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
|
|
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
|
|
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
|
|
252
|
+
/** Stream path to inject fault for */
|
|
252
253
|
path: string
|
|
253
|
-
/** HTTP status code to return */
|
|
254
|
-
status
|
|
255
|
-
/** Number of times to
|
|
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
|
|
@@ -143,3 +143,90 @@ tests:
|
|
|
143
143
|
expect:
|
|
144
144
|
minChunks: 1
|
|
145
145
|
upToDate: true
|
|
146
|
+
|
|
147
|
+
- id: sse-preserves-nel-character
|
|
148
|
+
name: SSE preserves NEL character (U+0085)
|
|
149
|
+
description: |
|
|
150
|
+
SSE parsers must NOT split on U+0085 (Next Line / NEL).
|
|
151
|
+
Per HTML Living Standard, SSE lines end only with CRLF, LF, or CR.
|
|
152
|
+
Other Unicode line separators must be preserved as data.
|
|
153
|
+
setup:
|
|
154
|
+
- action: create
|
|
155
|
+
as: streamPath
|
|
156
|
+
contentType: text/plain
|
|
157
|
+
- action: append
|
|
158
|
+
path: ${streamPath}
|
|
159
|
+
data: "before\u0085after"
|
|
160
|
+
operations:
|
|
161
|
+
- action: read
|
|
162
|
+
path: ${streamPath}
|
|
163
|
+
live: sse
|
|
164
|
+
waitForUpToDate: true
|
|
165
|
+
expect:
|
|
166
|
+
data: "before\u0085after"
|
|
167
|
+
minChunks: 1
|
|
168
|
+
|
|
169
|
+
- id: sse-preserves-line-separator
|
|
170
|
+
name: SSE preserves Line Separator (U+2028)
|
|
171
|
+
description: |
|
|
172
|
+
SSE parsers must NOT split on U+2028 (Line Separator).
|
|
173
|
+
This character can appear in JSON payloads and AI token streams.
|
|
174
|
+
It must be preserved as ordinary data, not treated as a line break.
|
|
175
|
+
setup:
|
|
176
|
+
- action: create
|
|
177
|
+
as: streamPath
|
|
178
|
+
contentType: text/plain
|
|
179
|
+
- action: append
|
|
180
|
+
path: ${streamPath}
|
|
181
|
+
data: "hello\u2028world"
|
|
182
|
+
operations:
|
|
183
|
+
- action: read
|
|
184
|
+
path: ${streamPath}
|
|
185
|
+
live: sse
|
|
186
|
+
waitForUpToDate: true
|
|
187
|
+
expect:
|
|
188
|
+
data: "hello\u2028world"
|
|
189
|
+
minChunks: 1
|
|
190
|
+
|
|
191
|
+
- id: sse-preserves-paragraph-separator
|
|
192
|
+
name: SSE preserves Paragraph Separator (U+2029)
|
|
193
|
+
description: |
|
|
194
|
+
SSE parsers must NOT split on U+2029 (Paragraph Separator).
|
|
195
|
+
This character can appear in JSON payloads and AI token streams.
|
|
196
|
+
It must be preserved as ordinary data, not treated as a line break.
|
|
197
|
+
setup:
|
|
198
|
+
- action: create
|
|
199
|
+
as: streamPath
|
|
200
|
+
contentType: text/plain
|
|
201
|
+
- action: append
|
|
202
|
+
path: ${streamPath}
|
|
203
|
+
data: "para1\u2029para2"
|
|
204
|
+
operations:
|
|
205
|
+
- action: read
|
|
206
|
+
path: ${streamPath}
|
|
207
|
+
live: sse
|
|
208
|
+
waitForUpToDate: true
|
|
209
|
+
expect:
|
|
210
|
+
data: "para1\u2029para2"
|
|
211
|
+
minChunks: 1
|
|
212
|
+
|
|
213
|
+
- id: sse-preserves-all-unicode-separators
|
|
214
|
+
name: SSE preserves all Unicode line separators together
|
|
215
|
+
description: |
|
|
216
|
+
Combined test ensuring NEL (U+0085), LS (U+2028), and PS (U+2029)
|
|
217
|
+
are all preserved in the same payload without being split.
|
|
218
|
+
setup:
|
|
219
|
+
- action: create
|
|
220
|
+
as: streamPath
|
|
221
|
+
contentType: text/plain
|
|
222
|
+
- action: append
|
|
223
|
+
path: ${streamPath}
|
|
224
|
+
data: "a\u0085b\u2028c\u2029d"
|
|
225
|
+
operations:
|
|
226
|
+
- action: read
|
|
227
|
+
path: ${streamPath}
|
|
228
|
+
live: sse
|
|
229
|
+
waitForUpToDate: true
|
|
230
|
+
expect:
|
|
231
|
+
data: "a\u0085b\u2028c\u2029d"
|
|
232
|
+
minChunks: 1
|