@durable-streams/client-conformance-tests 0.1.7 → 0.1.9
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 +72 -22
- package/dist/adapters/typescript-adapter.js +72 -22
- package/dist/{benchmark-runner-CrE6JkbX.js → benchmark-runner-81waaCzs.js} +89 -9
- package/dist/{benchmark-runner-Db4he452.cjs → benchmark-runner-DliEfq9k.cjs} +93 -8
- package/dist/cli.cjs +41 -5
- package/dist/cli.js +41 -5
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +50 -3
- package/dist/index.d.ts +50 -3
- package/dist/index.js +2 -2
- package/dist/{protocol-qb83AeUH.js → protocol-1p0soayz.js} +2 -1
- package/dist/{protocol-D37G3c4e.d.cts → protocol-BxZTqJmO.d.cts} +67 -5
- package/dist/{protocol-XeAOKBD-.cjs → protocol-IioVPNaP.cjs} +2 -1
- package/dist/{protocol-Mcbiq3nQ.d.ts → protocol-JuFzdV5x.d.ts} +67 -5
- package/dist/protocol.cjs +1 -1
- package/dist/protocol.d.cts +2 -2
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +1 -1
- package/package.json +8 -3
- package/src/adapters/typescript-adapter.ts +110 -32
- package/src/benchmark-runner.ts +75 -1
- package/src/benchmark-scenarios.ts +4 -4
- package/src/cli.ts +46 -5
- package/src/protocol.ts +75 -2
- package/src/runner.ts +72 -1
- package/src/test-cases.ts +55 -0
- package/test-cases/consumer/error-context.yaml +67 -0
- package/test-cases/consumer/json-parsing-errors.yaml +115 -0
- package/test-cases/consumer/read-auto.yaml +155 -0
- package/test-cases/consumer/read-sse.yaml +24 -0
- package/test-cases/consumer/retry-resilience.yaml +28 -0
- package/test-cases/consumer/sse-parsing-errors.yaml +121 -0
- package/test-cases/producer/error-context.yaml +72 -0
- package/test-cases/producer/idempotent-json-batching.yaml +40 -0
- package/test-cases/validation/input-validation.yaml +192 -0
|
@@ -190,7 +190,7 @@ export const smallMessageThroughputScenario: BenchmarkScenario = {
|
|
|
190
190
|
createOperation: (ctx) => ({
|
|
191
191
|
op: `throughput_append`,
|
|
192
192
|
path: `${ctx.basePath}/throughput-small`,
|
|
193
|
-
count:
|
|
193
|
+
count: 100000,
|
|
194
194
|
size: 100,
|
|
195
195
|
concurrency: 200,
|
|
196
196
|
}),
|
|
@@ -239,9 +239,9 @@ export const readThroughputScenario: BenchmarkScenario = {
|
|
|
239
239
|
expectedCount: ctx.setupData.expectedCount as number | undefined,
|
|
240
240
|
}),
|
|
241
241
|
setup: (ctx) => {
|
|
242
|
-
// Expecting
|
|
243
|
-
ctx.setupData.expectedCount =
|
|
244
|
-
return Promise.resolve({ data: { expectedCount:
|
|
242
|
+
// Expecting 100000 JSON messages to be pre-populated
|
|
243
|
+
ctx.setupData.expectedCount = 100000
|
|
244
|
+
return Promise.resolve({ data: { expectedCount: 100000 } })
|
|
245
245
|
},
|
|
246
246
|
}
|
|
247
247
|
|
package/src/cli.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { runConformanceTests } from "./runner.js"
|
|
13
|
-
import { runBenchmarks } from "./benchmark-runner.js"
|
|
13
|
+
import { aggregateBenchmarkResults, runBenchmarks } from "./benchmark-runner.js"
|
|
14
14
|
import type { RunnerOptions } from "./runner.js"
|
|
15
15
|
import type { BenchmarkRunnerOptions } from "./benchmark-runner.js"
|
|
16
16
|
|
|
@@ -20,6 +20,7 @@ Durable Streams Client Conformance Test Suite
|
|
|
20
20
|
Usage:
|
|
21
21
|
npx @durable-streams/client-conformance-tests --run <adapter> [options]
|
|
22
22
|
npx @durable-streams/client-conformance-tests --bench <adapter> [options]
|
|
23
|
+
npx @durable-streams/client-conformance-tests --report <dir>
|
|
23
24
|
|
|
24
25
|
Arguments:
|
|
25
26
|
<adapter> Path to client adapter executable, or "ts" for built-in TypeScript adapter
|
|
@@ -41,6 +42,10 @@ Benchmark Options:
|
|
|
41
42
|
Can be specified multiple times
|
|
42
43
|
--format <fmt> Output format: console, json, markdown (default: console)
|
|
43
44
|
|
|
45
|
+
Report Options:
|
|
46
|
+
--report <dir> Aggregate benchmark results from JSON files in directory
|
|
47
|
+
Each subdirectory should contain a benchmark-results.json file
|
|
48
|
+
|
|
44
49
|
Common Options:
|
|
45
50
|
--verbose Show detailed output for each operation
|
|
46
51
|
--port <port> Port for reference server (default: random)
|
|
@@ -72,6 +77,10 @@ Benchmark Examples:
|
|
|
72
77
|
# Output as JSON for CI
|
|
73
78
|
npx @durable-streams/client-conformance-tests --bench ts --format json
|
|
74
79
|
|
|
80
|
+
Report Examples:
|
|
81
|
+
# Aggregate benchmark results from CI artifacts
|
|
82
|
+
npx @durable-streams/client-conformance-tests --report ./benchmark-results
|
|
83
|
+
|
|
75
84
|
Implementing a Client Adapter:
|
|
76
85
|
A client adapter is an executable that communicates via stdin/stdout using
|
|
77
86
|
JSON-line protocol. See the documentation for the protocol specification
|
|
@@ -93,11 +102,13 @@ Implementing a Client Adapter:
|
|
|
93
102
|
type ParsedOptions =
|
|
94
103
|
| { mode: `conformance`; options: RunnerOptions }
|
|
95
104
|
| { mode: `benchmark`; options: BenchmarkRunnerOptions }
|
|
105
|
+
| { mode: `report`; resultsDir: string }
|
|
96
106
|
| null
|
|
97
107
|
|
|
98
108
|
function parseArgs(args: Array<string>): ParsedOptions {
|
|
99
|
-
let mode: `conformance` | `benchmark` | null = null
|
|
109
|
+
let mode: `conformance` | `benchmark` | `report` | null = null
|
|
100
110
|
let clientAdapter = ``
|
|
111
|
+
let resultsDir = ``
|
|
101
112
|
|
|
102
113
|
// Conformance-specific options
|
|
103
114
|
const suites: Array<`producer` | `consumer` | `lifecycle`> = []
|
|
@@ -139,6 +150,14 @@ function parseArgs(args: Array<string>): ParsedOptions {
|
|
|
139
150
|
return null
|
|
140
151
|
}
|
|
141
152
|
clientAdapter = args[i]!
|
|
153
|
+
} else if (arg === `--report`) {
|
|
154
|
+
mode = `report`
|
|
155
|
+
i++
|
|
156
|
+
if (i >= args.length) {
|
|
157
|
+
console.error(`Error: --report requires a directory path`)
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
resultsDir = args[i]!
|
|
142
161
|
} else if (arg === `--suite`) {
|
|
143
162
|
i++
|
|
144
163
|
if (i >= args.length) {
|
|
@@ -230,8 +249,26 @@ function parseArgs(args: Array<string>): ParsedOptions {
|
|
|
230
249
|
}
|
|
231
250
|
|
|
232
251
|
// Validate required options
|
|
233
|
-
if (!mode
|
|
234
|
-
console.error(
|
|
252
|
+
if (!mode) {
|
|
253
|
+
console.error(
|
|
254
|
+
`Error: --run <adapter>, --bench <adapter>, or --report <dir> is required`
|
|
255
|
+
)
|
|
256
|
+
console.log(`\nRun with --help for usage information`)
|
|
257
|
+
return null
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (mode === `report`) {
|
|
261
|
+
if (!resultsDir) {
|
|
262
|
+
console.error(`Error: --report requires a directory path`)
|
|
263
|
+
return null
|
|
264
|
+
}
|
|
265
|
+
return { mode: `report`, resultsDir }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (!clientAdapter) {
|
|
269
|
+
console.error(
|
|
270
|
+
`Error: --run <adapter> or --bench <adapter> requires an adapter path`
|
|
271
|
+
)
|
|
235
272
|
console.log(`\nRun with --help for usage information`)
|
|
236
273
|
return null
|
|
237
274
|
}
|
|
@@ -279,11 +316,15 @@ async function main(): Promise<void> {
|
|
|
279
316
|
if (summary.failed > 0) {
|
|
280
317
|
process.exit(1)
|
|
281
318
|
}
|
|
282
|
-
} else {
|
|
319
|
+
} else if (parsed.mode === `benchmark`) {
|
|
283
320
|
const summary = await runBenchmarks(parsed.options)
|
|
284
321
|
if (summary.failed > 0) {
|
|
285
322
|
process.exit(1)
|
|
286
323
|
}
|
|
324
|
+
} else {
|
|
325
|
+
// parsed.mode === `report`
|
|
326
|
+
const report = await aggregateBenchmarkResults(parsed.resultsDir)
|
|
327
|
+
console.log(report)
|
|
287
328
|
}
|
|
288
329
|
} catch (err) {
|
|
289
330
|
console.error(`Error running ${parsed.mode}:`, err)
|
package/src/protocol.ts
CHANGED
|
@@ -120,8 +120,8 @@ export interface ReadCommand {
|
|
|
120
120
|
path: string
|
|
121
121
|
/** Starting offset (opaque string from previous reads) */
|
|
122
122
|
offset?: string
|
|
123
|
-
/** Live mode: false for catch-up only, "long-poll" or "sse" for
|
|
124
|
-
live?: false | `long-poll` | `sse`
|
|
123
|
+
/** Live mode: false for catch-up only, true for auto-select, "long-poll" or "sse" for explicit */
|
|
124
|
+
live?: false | true | `long-poll` | `sse`
|
|
125
125
|
/** Timeout for long-poll in milliseconds */
|
|
126
126
|
timeoutMs?: number
|
|
127
127
|
/** Maximum number of chunks to read (for testing) */
|
|
@@ -196,6 +196,59 @@ export interface ClearDynamicCommand {
|
|
|
196
196
|
type: `clear-dynamic`
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
// =============================================================================
|
|
200
|
+
// Validation Commands
|
|
201
|
+
// =============================================================================
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Test client-side input validation.
|
|
205
|
+
*
|
|
206
|
+
* This command tests that the client properly validates input parameters
|
|
207
|
+
* before making any network requests. The adapter should attempt to create
|
|
208
|
+
* the specified object with the given parameters and report whether
|
|
209
|
+
* validation passed or failed.
|
|
210
|
+
*/
|
|
211
|
+
export interface ValidateCommand {
|
|
212
|
+
type: `validate`
|
|
213
|
+
/** What to validate */
|
|
214
|
+
target: ValidateTarget
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Validation targets - what client-side validation to test.
|
|
219
|
+
*/
|
|
220
|
+
export type ValidateTarget = ValidateRetryOptions | ValidateIdempotentProducer
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate RetryOptions construction.
|
|
224
|
+
*/
|
|
225
|
+
export interface ValidateRetryOptions {
|
|
226
|
+
target: `retry-options`
|
|
227
|
+
/** Max retries (should reject < 0) */
|
|
228
|
+
maxRetries?: number
|
|
229
|
+
/** Initial delay in ms (should reject <= 0) */
|
|
230
|
+
initialDelayMs?: number
|
|
231
|
+
/** Max delay in ms (should reject < initialDelayMs) */
|
|
232
|
+
maxDelayMs?: number
|
|
233
|
+
/** Backoff multiplier (should reject < 1.0) */
|
|
234
|
+
multiplier?: number
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate IdempotentProducer construction.
|
|
239
|
+
*/
|
|
240
|
+
export interface ValidateIdempotentProducer {
|
|
241
|
+
target: `idempotent-producer`
|
|
242
|
+
/** Producer ID (required, non-empty) */
|
|
243
|
+
producerId?: string
|
|
244
|
+
/** Starting epoch (should reject < 0) */
|
|
245
|
+
epoch?: number
|
|
246
|
+
/** Max batch bytes (should reject <= 0) */
|
|
247
|
+
maxBatchBytes?: number
|
|
248
|
+
/** Max batch items (should reject <= 0) */
|
|
249
|
+
maxBatchItems?: number
|
|
250
|
+
}
|
|
251
|
+
|
|
199
252
|
// =============================================================================
|
|
200
253
|
// Benchmark Commands
|
|
201
254
|
// =============================================================================
|
|
@@ -289,6 +342,7 @@ export type TestCommand =
|
|
|
289
342
|
| SetDynamicParamCommand
|
|
290
343
|
| ClearDynamicCommand
|
|
291
344
|
| BenchmarkCommand
|
|
345
|
+
| ValidateCommand
|
|
292
346
|
|
|
293
347
|
// =============================================================================
|
|
294
348
|
// Results (sent from client adapter to test runner via stdout)
|
|
@@ -312,10 +366,18 @@ export interface InitResult {
|
|
|
312
366
|
sse?: boolean
|
|
313
367
|
/** Supports long-poll mode */
|
|
314
368
|
longPoll?: boolean
|
|
369
|
+
/** Supports auto mode (catch-up then auto-select SSE or long-poll) */
|
|
370
|
+
auto?: boolean
|
|
315
371
|
/** Supports streaming reads */
|
|
316
372
|
streaming?: boolean
|
|
317
373
|
/** Supports dynamic headers/params (functions evaluated per-request) */
|
|
318
374
|
dynamicHeaders?: boolean
|
|
375
|
+
/** Supports RetryOptions validation (PHP-specific) */
|
|
376
|
+
retryOptions?: boolean
|
|
377
|
+
/** Supports maxBatchItems option (PHP-specific) */
|
|
378
|
+
batchItems?: boolean
|
|
379
|
+
/** Rejects zero values as invalid (vs treating 0 as "use default" like Go) */
|
|
380
|
+
strictZeroValidation?: boolean
|
|
319
381
|
}
|
|
320
382
|
}
|
|
321
383
|
|
|
@@ -492,6 +554,14 @@ export interface ClearDynamicResult {
|
|
|
492
554
|
success: true
|
|
493
555
|
}
|
|
494
556
|
|
|
557
|
+
/**
|
|
558
|
+
* Successful validate result (validation passed).
|
|
559
|
+
*/
|
|
560
|
+
export interface ValidateResult {
|
|
561
|
+
type: `validate`
|
|
562
|
+
success: true
|
|
563
|
+
}
|
|
564
|
+
|
|
495
565
|
/**
|
|
496
566
|
* Successful benchmark result with timing.
|
|
497
567
|
*/
|
|
@@ -549,6 +619,7 @@ export type TestResult =
|
|
|
549
619
|
| SetDynamicHeaderResult
|
|
550
620
|
| SetDynamicParamResult
|
|
551
621
|
| ClearDynamicResult
|
|
622
|
+
| ValidateResult
|
|
552
623
|
| BenchmarkResult
|
|
553
624
|
| ErrorResult
|
|
554
625
|
|
|
@@ -622,6 +693,8 @@ export const ErrorCodes = {
|
|
|
622
693
|
INTERNAL_ERROR: `INTERNAL_ERROR`,
|
|
623
694
|
/** Operation not supported by this client */
|
|
624
695
|
NOT_SUPPORTED: `NOT_SUPPORTED`,
|
|
696
|
+
/** Invalid argument passed to client API */
|
|
697
|
+
INVALID_ARGUMENT: `INVALID_ARGUMENT`,
|
|
625
698
|
} as const
|
|
626
699
|
|
|
627
700
|
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
|
package/src/runner.ts
CHANGED
|
@@ -77,8 +77,12 @@ interface ClientFeatures {
|
|
|
77
77
|
batching?: boolean
|
|
78
78
|
sse?: boolean
|
|
79
79
|
longPoll?: boolean
|
|
80
|
+
auto?: boolean
|
|
80
81
|
streaming?: boolean
|
|
81
82
|
dynamicHeaders?: boolean
|
|
83
|
+
retryOptions?: boolean
|
|
84
|
+
batchItems?: boolean
|
|
85
|
+
strictZeroValidation?: boolean
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
interface ExecutionContext {
|
|
@@ -300,7 +304,13 @@ async function executeOperation(
|
|
|
300
304
|
|
|
301
305
|
case `append`: {
|
|
302
306
|
const path = resolveVariables(op.path, variables)
|
|
303
|
-
|
|
307
|
+
// Handle json property (YAML object) by stringifying, or use data string directly
|
|
308
|
+
const data =
|
|
309
|
+
op.json !== undefined
|
|
310
|
+
? JSON.stringify(op.json)
|
|
311
|
+
: op.data
|
|
312
|
+
? resolveVariables(op.data, variables)
|
|
313
|
+
: ``
|
|
304
314
|
|
|
305
315
|
const result = await client.send(
|
|
306
316
|
{
|
|
@@ -729,6 +739,7 @@ async function executeOperation(
|
|
|
729
739
|
method: op.method,
|
|
730
740
|
corruptBody: op.corruptBody,
|
|
731
741
|
jitterMs: op.jitterMs,
|
|
742
|
+
injectSseEvent: op.injectSseEvent,
|
|
732
743
|
}),
|
|
733
744
|
})
|
|
734
745
|
|
|
@@ -742,6 +753,8 @@ async function executeOperation(
|
|
|
742
753
|
faultTypes.push(`truncate=${op.truncateBodyBytes}b`)
|
|
743
754
|
if (op.corruptBody) faultTypes.push(`corrupt`)
|
|
744
755
|
if (op.probability != null) faultTypes.push(`p=${op.probability}`)
|
|
756
|
+
if (op.injectSseEvent)
|
|
757
|
+
faultTypes.push(`sse:${op.injectSseEvent.eventType}`)
|
|
745
758
|
const faultDesc = faultTypes.join(`,`) || `unknown`
|
|
746
759
|
|
|
747
760
|
if (verbose) {
|
|
@@ -835,6 +848,25 @@ async function executeOperation(
|
|
|
835
848
|
return { result }
|
|
836
849
|
}
|
|
837
850
|
|
|
851
|
+
case `validate`: {
|
|
852
|
+
const result = await client.send(
|
|
853
|
+
{
|
|
854
|
+
type: `validate`,
|
|
855
|
+
target: op.target,
|
|
856
|
+
},
|
|
857
|
+
commandTimeout
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
if (verbose) {
|
|
861
|
+
const targetType = op.target.target
|
|
862
|
+
console.log(
|
|
863
|
+
` validate ${targetType}: ${result.success ? `ok` : `failed`}`
|
|
864
|
+
)
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return { result }
|
|
868
|
+
}
|
|
869
|
+
|
|
838
870
|
default:
|
|
839
871
|
return { error: `Unknown operation: ${(op as TestOperation).action}` }
|
|
840
872
|
}
|
|
@@ -879,6 +911,21 @@ function validateExpectation(
|
|
|
879
911
|
}
|
|
880
912
|
}
|
|
881
913
|
|
|
914
|
+
// Check error message contains expected strings
|
|
915
|
+
if (expect.messageContains !== undefined) {
|
|
916
|
+
if (result.success) {
|
|
917
|
+
return `Expected error with message containing ${JSON.stringify(expect.messageContains)}, but operation succeeded`
|
|
918
|
+
}
|
|
919
|
+
if (isErrorResult(result)) {
|
|
920
|
+
const missing = (expect.messageContains as Array<string>).filter(
|
|
921
|
+
(s) => !result.message.toLowerCase().includes(s.toLowerCase())
|
|
922
|
+
)
|
|
923
|
+
if (missing.length > 0) {
|
|
924
|
+
return `Expected error message to contain [${(expect.messageContains as Array<string>).join(`, `)}], missing: [${missing.join(`, `)}]. Actual message: "${result.message}"`
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
882
929
|
// Check data (for read results)
|
|
883
930
|
if (expect.data !== undefined && isReadResult(result)) {
|
|
884
931
|
const actualData = result.chunks.map((c) => c.data).join(``)
|
|
@@ -1039,6 +1086,23 @@ function validateExpectation(
|
|
|
1039
1086
|
}
|
|
1040
1087
|
}
|
|
1041
1088
|
|
|
1089
|
+
// Check valid (for validation operations)
|
|
1090
|
+
if (expect.valid !== undefined) {
|
|
1091
|
+
if (expect.valid === true && !result.success) {
|
|
1092
|
+
return `Expected validation to pass, but it failed`
|
|
1093
|
+
}
|
|
1094
|
+
if (expect.valid === false && result.success) {
|
|
1095
|
+
return `Expected validation to fail, but it passed`
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Check errorContains (for validation operations with error message substring)
|
|
1100
|
+
if (expect.errorContains !== undefined && isErrorResult(result)) {
|
|
1101
|
+
if (!result.message.includes(expect.errorContains as string)) {
|
|
1102
|
+
return `Expected error message to contain "${expect.errorContains}", got "${result.message}"`
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1042
1106
|
return null
|
|
1043
1107
|
}
|
|
1044
1108
|
|
|
@@ -1051,9 +1115,16 @@ function featureToProperty(feature: string): keyof ClientFeatures | undefined {
|
|
|
1051
1115
|
sse: `sse`,
|
|
1052
1116
|
"long-poll": `longPoll`,
|
|
1053
1117
|
longPoll: `longPoll`,
|
|
1118
|
+
auto: `auto`,
|
|
1054
1119
|
streaming: `streaming`,
|
|
1055
1120
|
dynamicHeaders: `dynamicHeaders`,
|
|
1056
1121
|
"dynamic-headers": `dynamicHeaders`,
|
|
1122
|
+
retryOptions: `retryOptions`,
|
|
1123
|
+
"retry-options": `retryOptions`,
|
|
1124
|
+
batchItems: `batchItems`,
|
|
1125
|
+
"batch-items": `batchItems`,
|
|
1126
|
+
strictZeroValidation: `strictZeroValidation`,
|
|
1127
|
+
"strict-zero-validation": `strictZeroValidation`,
|
|
1057
1128
|
}
|
|
1058
1129
|
return map[feature]
|
|
1059
1130
|
}
|
package/src/test-cases.ts
CHANGED
|
@@ -116,6 +116,8 @@ export interface AppendOperation {
|
|
|
116
116
|
path: string
|
|
117
117
|
/** Data to append (string) */
|
|
118
118
|
data?: string
|
|
119
|
+
/** JSON data to append (will be stringified) */
|
|
120
|
+
json?: unknown
|
|
119
121
|
/** Binary data (base64 encoded) */
|
|
120
122
|
binaryData?: string
|
|
121
123
|
/** Sequence number for ordering (Stream-Seq header) */
|
|
@@ -364,6 +366,13 @@ export interface InjectErrorOperation {
|
|
|
364
366
|
corruptBody?: boolean
|
|
365
367
|
/** Add jitter to delay (random 0-jitterMs added to delayMs) */
|
|
366
368
|
jitterMs?: number
|
|
369
|
+
/** Inject an SSE event with custom type and data (for testing SSE parsing) */
|
|
370
|
+
injectSseEvent?: {
|
|
371
|
+
/** Event type (e.g., "unknown", "control", "data") */
|
|
372
|
+
eventType: string
|
|
373
|
+
/** Event data (will be sent as-is) */
|
|
374
|
+
data: string
|
|
375
|
+
}
|
|
367
376
|
}
|
|
368
377
|
|
|
369
378
|
/**
|
|
@@ -405,6 +414,49 @@ export interface ClearDynamicOperation {
|
|
|
405
414
|
action: `clear-dynamic`
|
|
406
415
|
}
|
|
407
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Validate client-side input parameters.
|
|
419
|
+
* Tests that clients properly validate inputs before making network requests.
|
|
420
|
+
*/
|
|
421
|
+
export interface ValidateOperation {
|
|
422
|
+
action: `validate`
|
|
423
|
+
/** What to validate */
|
|
424
|
+
target: ValidateTarget
|
|
425
|
+
expect?: ValidateExpectation
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Validation target types.
|
|
430
|
+
*/
|
|
431
|
+
export type ValidateTarget =
|
|
432
|
+
| ValidateRetryOptionsTarget
|
|
433
|
+
| ValidateIdempotentProducerTarget
|
|
434
|
+
|
|
435
|
+
export interface ValidateRetryOptionsTarget {
|
|
436
|
+
target: `retry-options`
|
|
437
|
+
maxRetries?: number
|
|
438
|
+
initialDelayMs?: number
|
|
439
|
+
maxDelayMs?: number
|
|
440
|
+
multiplier?: number
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export interface ValidateIdempotentProducerTarget {
|
|
444
|
+
target: `idempotent-producer`
|
|
445
|
+
producerId?: string
|
|
446
|
+
epoch?: number
|
|
447
|
+
maxBatchBytes?: number
|
|
448
|
+
maxBatchItems?: number
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export interface ValidateExpectation {
|
|
452
|
+
/** If true, validation should pass */
|
|
453
|
+
valid?: boolean
|
|
454
|
+
/** Expected error code if validation fails */
|
|
455
|
+
errorCode?: string
|
|
456
|
+
/** Expected error message substring if validation fails */
|
|
457
|
+
errorContains?: string
|
|
458
|
+
}
|
|
459
|
+
|
|
408
460
|
/**
|
|
409
461
|
* All possible test operations.
|
|
410
462
|
*/
|
|
@@ -428,6 +480,7 @@ export type TestOperation =
|
|
|
428
480
|
| SetDynamicHeaderOperation
|
|
429
481
|
| SetDynamicParamOperation
|
|
430
482
|
| ClearDynamicOperation
|
|
483
|
+
| ValidateOperation
|
|
431
484
|
|
|
432
485
|
// =============================================================================
|
|
433
486
|
// Expectations
|
|
@@ -441,6 +494,8 @@ interface BaseExpectation {
|
|
|
441
494
|
status?: number
|
|
442
495
|
/** Expected error code (if operation should fail) */
|
|
443
496
|
errorCode?: string
|
|
497
|
+
/** Strings that should be present in error message (for context validation) */
|
|
498
|
+
messageContains?: Array<string>
|
|
444
499
|
/** Store result in variable */
|
|
445
500
|
storeAs?: string
|
|
446
501
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
id: consumer-error-context
|
|
2
|
+
name: Consumer Error Context
|
|
3
|
+
description: |
|
|
4
|
+
Tests that error messages include helpful context (URL, status, etc.).
|
|
5
|
+
Catching bugs where errors lose context or provide unhelpful messages.
|
|
6
|
+
category: consumer
|
|
7
|
+
tags:
|
|
8
|
+
- errors
|
|
9
|
+
- context
|
|
10
|
+
- messages
|
|
11
|
+
|
|
12
|
+
tests:
|
|
13
|
+
- id: not-found-includes-path
|
|
14
|
+
name: 404 error includes stream path in message
|
|
15
|
+
description: Error message should include the stream path for debugging
|
|
16
|
+
operations:
|
|
17
|
+
- action: read
|
|
18
|
+
path: /error-context-nonexistent-stream
|
|
19
|
+
live: false
|
|
20
|
+
expect:
|
|
21
|
+
status: 404
|
|
22
|
+
errorCode: NOT_FOUND
|
|
23
|
+
messageContains:
|
|
24
|
+
- "error-context-nonexistent-stream"
|
|
25
|
+
|
|
26
|
+
- id: not-found-on-append-includes-path
|
|
27
|
+
name: 404 on append includes stream path in message
|
|
28
|
+
description: Append to non-existent stream should report the path
|
|
29
|
+
operations:
|
|
30
|
+
- action: append
|
|
31
|
+
path: /error-context-append-nonexistent
|
|
32
|
+
data: "test"
|
|
33
|
+
expect:
|
|
34
|
+
status: 404
|
|
35
|
+
errorCode: NOT_FOUND
|
|
36
|
+
messageContains:
|
|
37
|
+
- "error-context-append-nonexistent"
|
|
38
|
+
|
|
39
|
+
- id: invalid-offset-includes-context
|
|
40
|
+
name: Invalid offset error includes helpful context
|
|
41
|
+
description: Error for invalid offset should indicate the nature of the error
|
|
42
|
+
setup:
|
|
43
|
+
- action: create
|
|
44
|
+
as: streamPath
|
|
45
|
+
- action: append
|
|
46
|
+
path: ${streamPath}
|
|
47
|
+
data: "data"
|
|
48
|
+
operations:
|
|
49
|
+
- action: read
|
|
50
|
+
path: ${streamPath}
|
|
51
|
+
offset: "not-a-valid-offset-format"
|
|
52
|
+
live: false
|
|
53
|
+
expect:
|
|
54
|
+
status: 400
|
|
55
|
+
errorCode: INVALID_OFFSET
|
|
56
|
+
|
|
57
|
+
- id: head-not-found-includes-path
|
|
58
|
+
name: HEAD 404 error includes stream path
|
|
59
|
+
description: HEAD request to non-existent stream should include path in error
|
|
60
|
+
operations:
|
|
61
|
+
- action: head
|
|
62
|
+
path: /error-context-head-nonexistent
|
|
63
|
+
expect:
|
|
64
|
+
status: 404
|
|
65
|
+
errorCode: NOT_FOUND
|
|
66
|
+
messageContains:
|
|
67
|
+
- "error-context-head-nonexistent"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
id: consumer-json-parsing-errors
|
|
2
|
+
name: JSON Parsing Error Handling
|
|
3
|
+
description: Tests that clients properly handle malformed JSON responses instead of silently failing
|
|
4
|
+
category: consumer
|
|
5
|
+
tags:
|
|
6
|
+
- json
|
|
7
|
+
- error-handling
|
|
8
|
+
- resilience
|
|
9
|
+
- fault-injection
|
|
10
|
+
|
|
11
|
+
tests:
|
|
12
|
+
- id: truncated-json-response
|
|
13
|
+
name: Client throws on truncated JSON response
|
|
14
|
+
description: Client should throw PARSE_ERROR for incomplete JSON, not return empty array
|
|
15
|
+
requiredFeatures: [json]
|
|
16
|
+
setup:
|
|
17
|
+
- action: create
|
|
18
|
+
as: streamPath
|
|
19
|
+
contentType: application/json
|
|
20
|
+
- action: append
|
|
21
|
+
path: ${streamPath}
|
|
22
|
+
json: { "key": "some long value that will be truncated" }
|
|
23
|
+
operations:
|
|
24
|
+
# Truncate the response mid-JSON (10 bytes is mid-object)
|
|
25
|
+
- action: inject-error
|
|
26
|
+
path: ${streamPath}
|
|
27
|
+
truncateBodyBytes: 10
|
|
28
|
+
method: GET
|
|
29
|
+
count: 1
|
|
30
|
+
- action: read
|
|
31
|
+
path: ${streamPath}
|
|
32
|
+
expect:
|
|
33
|
+
errorCode: PARSE_ERROR
|
|
34
|
+
cleanup:
|
|
35
|
+
- action: clear-errors
|
|
36
|
+
|
|
37
|
+
- id: corrupted-json-response
|
|
38
|
+
name: Client throws on corrupted JSON response
|
|
39
|
+
description: Client should throw PARSE_ERROR for invalid JSON, not return empty array
|
|
40
|
+
requiredFeatures: [json]
|
|
41
|
+
setup:
|
|
42
|
+
- action: create
|
|
43
|
+
as: streamPath
|
|
44
|
+
contentType: application/json
|
|
45
|
+
- action: append
|
|
46
|
+
path: ${streamPath}
|
|
47
|
+
json: { "valid": "json data here" }
|
|
48
|
+
operations:
|
|
49
|
+
# Corrupt the JSON by flipping bits
|
|
50
|
+
- action: inject-error
|
|
51
|
+
path: ${streamPath}
|
|
52
|
+
corruptBody: true
|
|
53
|
+
method: GET
|
|
54
|
+
count: 1
|
|
55
|
+
- action: read
|
|
56
|
+
path: ${streamPath}
|
|
57
|
+
expect:
|
|
58
|
+
errorCode: PARSE_ERROR
|
|
59
|
+
cleanup:
|
|
60
|
+
- action: clear-errors
|
|
61
|
+
|
|
62
|
+
- id: truncated-json-array
|
|
63
|
+
name: Client throws on truncated JSON array
|
|
64
|
+
description: Client should throw when JSON array is truncated mid-element
|
|
65
|
+
requiredFeatures: [json]
|
|
66
|
+
setup:
|
|
67
|
+
- action: create
|
|
68
|
+
as: streamPath
|
|
69
|
+
contentType: application/json
|
|
70
|
+
- action: append
|
|
71
|
+
path: ${streamPath}
|
|
72
|
+
json: [{ "item": 1 }, { "item": 2 }, { "item": 3 }]
|
|
73
|
+
operations:
|
|
74
|
+
# Truncate mid-array
|
|
75
|
+
- action: inject-error
|
|
76
|
+
path: ${streamPath}
|
|
77
|
+
truncateBodyBytes: 15
|
|
78
|
+
method: GET
|
|
79
|
+
count: 1
|
|
80
|
+
- action: read
|
|
81
|
+
path: ${streamPath}
|
|
82
|
+
expect:
|
|
83
|
+
errorCode: PARSE_ERROR
|
|
84
|
+
cleanup:
|
|
85
|
+
- action: clear-errors
|
|
86
|
+
|
|
87
|
+
- id: recovery-after-parse-error
|
|
88
|
+
name: Client recovers after parse error
|
|
89
|
+
description: After a parse error, subsequent requests should succeed
|
|
90
|
+
requiredFeatures: [json]
|
|
91
|
+
setup:
|
|
92
|
+
- action: create
|
|
93
|
+
as: streamPath
|
|
94
|
+
contentType: application/json
|
|
95
|
+
- action: append
|
|
96
|
+
path: ${streamPath}
|
|
97
|
+
json: { "data": "valid" }
|
|
98
|
+
operations:
|
|
99
|
+
# First request fails with truncated body
|
|
100
|
+
- action: inject-error
|
|
101
|
+
path: ${streamPath}
|
|
102
|
+
truncateBodyBytes: 5
|
|
103
|
+
method: GET
|
|
104
|
+
count: 1
|
|
105
|
+
- action: read
|
|
106
|
+
path: ${streamPath}
|
|
107
|
+
expect:
|
|
108
|
+
errorCode: PARSE_ERROR
|
|
109
|
+
# Second request should succeed (no more faults)
|
|
110
|
+
- action: read
|
|
111
|
+
path: ${streamPath}
|
|
112
|
+
expect:
|
|
113
|
+
status: 200
|
|
114
|
+
cleanup:
|
|
115
|
+
- action: clear-errors
|