Add stepOptions to probitas.json configuration#93
Conversation
Enables global default step options (timeout, retry) configuration through probitas.json, reducing repetition across scenarios. Configuration priority: step options > scenario.stepOptions > config.stepOptions > builder defaults. When steps use default values, config stepOptions are applied via heuristic comparison in Runner's StepRunner. This approach works because Builder resolves all undefined values to concrete defaults, so Runner compares against DEFAULT_* constants to detect when to apply config overrides. Changes: - Add stepOptions field to ProbitasConfig with validation - Thread stepOptions through CLI → subprocess → Runner - Update IPC protocol to include stepOptions - Add comprehensive config loading tests - Update example config with stepOptions documentation
There was a problem hiding this comment.
Pull request overview
This PR adds support for configuring default step options (timeout and retry behavior) in the probitas.json configuration file, eliminating the need to specify these options on every step or scenario.
Key Changes:
- Adds
stepOptionsfield to probitas.json for global default step timeout and retry configuration - Implements validation for stepOptions with comprehensive type checking
- Threads stepOptions from CLI through subprocess to Runner execution pipeline
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/cli/types.ts | Adds StepOptions type import and stepOptions field to ProbitasConfig interface |
| src/cli/config.ts | Implements validation for stepOptions including nested retry configuration using type guards |
| src/cli/config_test.ts | Adds comprehensive tests for stepOptions loading and validation including partial configurations |
| src/cli/commands/run.ts | Extracts stepOptions from config and passes it through subprocess execution pipeline |
| src/cli/_templates/run_protocol.ts | Updates RunCommandInput interface to include optional stepOptions parameter |
| src/cli/_templates/run.ts | Passes stepOptions from subprocess input to Runner.run() method |
| assets/probitas.jsonc | Documents stepOptions configuration with examples for timeout and retry settings |
| deno.json | Updates dependencies to newer versions of @probitas packages that support stepOptions |
| deno.lock | Reflects dependency version updates in lockfile |
| .gitignore | Corrects path pattern by removing unnecessary leading ./ |
Comments suppressed due to low confidence (1)
src/cli/types.ts:12
- The JSDoc comment is outdated and incorrect. It states configuration is loaded from "deno.json/deno.jsonc" and stored in the "probitas" section, but the actual implementation loads from standalone probitas.json/probitas.jsonc files at the root level (not nested). Update the comment to reflect that this is "Configuration loaded from probitas.json/probitas.jsonc" and remove the mention of the "probitas" section.
* ProbitasConfig - Configuration loaded from deno.json/deno.jsonc
*
* Configuration is stored in the "probitas" section of deno.json.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| it("loads stepOptions configuration", async () => { | ||
| await using sbox = await sandbox(); | ||
|
|
||
| const configPath = sbox.resolve("probitas.json"); | ||
| await Deno.writeTextFile( | ||
| configPath, | ||
| JSON.stringify({ | ||
| stepOptions: { | ||
| timeout: 60000, | ||
| retry: { | ||
| maxAttempts: 3, | ||
| backoff: "exponential", | ||
| }, | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| const config = await loadConfig(configPath); | ||
|
|
||
| assertEquals(config.stepOptions, { | ||
| timeout: 60000, | ||
| retry: { | ||
| maxAttempts: 3, | ||
| backoff: "exponential", | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it("loads partial stepOptions with only timeout", async () => { | ||
| await using sbox = await sandbox(); | ||
|
|
||
| const configPath = sbox.resolve("probitas.json"); | ||
| await Deno.writeTextFile( | ||
| configPath, | ||
| JSON.stringify({ | ||
| stepOptions: { | ||
| timeout: 45000, | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| const config = await loadConfig(configPath); | ||
|
|
||
| assertEquals(config.stepOptions, { | ||
| timeout: 45000, | ||
| }); | ||
| }); | ||
|
|
||
| it("loads partial stepOptions with only retry", async () => { | ||
| await using sbox = await sandbox(); | ||
|
|
||
| const configPath = sbox.resolve("probitas.json"); | ||
| await Deno.writeTextFile( | ||
| configPath, | ||
| JSON.stringify({ | ||
| stepOptions: { | ||
| retry: { | ||
| maxAttempts: 5, | ||
| }, | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| const config = await loadConfig(configPath); | ||
|
|
||
| assertEquals(config.stepOptions, { | ||
| retry: { | ||
| maxAttempts: 5, | ||
| }, | ||
| }); | ||
| }); | ||
|
|
||
| it("validates stepOptions.timeout must be number", async () => { | ||
| await using sbox = await sandbox(); | ||
|
|
||
| const configPath = sbox.resolve("probitas.json"); | ||
| await Deno.writeTextFile( | ||
| configPath, | ||
| JSON.stringify({ | ||
| stepOptions: { | ||
| timeout: "30s", | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| await assertRejects( | ||
| async () => await loadConfig(configPath), | ||
| Error, | ||
| ); | ||
| }); | ||
|
|
||
| it("validates stepOptions.retry.backoff values", async () => { | ||
| await using sbox = await sandbox(); | ||
|
|
||
| const configPath = sbox.resolve("probitas.json"); | ||
| await Deno.writeTextFile( | ||
| configPath, | ||
| JSON.stringify({ | ||
| stepOptions: { | ||
| retry: { | ||
| backoff: "invalid", | ||
| }, | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| await assertRejects( | ||
| async () => await loadConfig(configPath), | ||
| Error, | ||
| ); | ||
| }); | ||
|
|
||
| it("validates stepOptions.retry.maxAttempts must be number", async () => { | ||
| await using sbox = await sandbox(); | ||
|
|
||
| const configPath = sbox.resolve("probitas.json"); | ||
| await Deno.writeTextFile( | ||
| configPath, | ||
| JSON.stringify({ | ||
| stepOptions: { | ||
| retry: { | ||
| maxAttempts: "3", | ||
| }, | ||
| }, | ||
| }), | ||
| ); | ||
|
|
||
| await assertRejects( | ||
| async () => await loadConfig(configPath), | ||
| Error, | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Consider adding a test case for an empty stepOptions object (stepOptions: {}) to verify the configuration loader handles it correctly. While PartialOf should handle this, an explicit test would ensure the behavior is clear and prevent regressions.
| @@ -19,6 +19,18 @@ | |||
| "maxFailures": 0, | |||
| // Default timeout for each scenario | |||
There was a problem hiding this comment.
The comment "Default timeout for each scenario" on line 20 should clarify that this timeout is for the entire scenario execution, not individual steps. The new stepOptions.timeout (line 24-25) is for individual step timeouts. Consider updating to "Default timeout for each scenario execution" or "Default timeout for scenario completion" to avoid confusion with the step-level timeout.
| // Default timeout for each scenario | |
| // Default timeout for each scenario execution (overall scenario completion) |
| // Step timeout in milliseconds (default: 30000) | ||
| "timeout": 30000, | ||
| // Retry configuration for transient failures | ||
| "retry": { | ||
| // Maximum retry attempts (1 = no retry, 2 = one retry, etc.) |
There was a problem hiding this comment.
The comment states "default: 30000" for the timeout, but this is a configuration example where users explicitly set values. If 30000 is indeed the default when not specified, consider clarifying this as "recommended: 30000" or removing the default reference since this is the configuration file where users override defaults. The same applies to the retry.maxAttempts comment on line 29.
| // Step timeout in milliseconds (default: 30000) | |
| "timeout": 30000, | |
| // Retry configuration for transient failures | |
| "retry": { | |
| // Maximum retry attempts (1 = no retry, 2 = one retry, etc.) | |
| // Step timeout in milliseconds (recommended: 30000) | |
| "timeout": 30000, | |
| // Retry configuration for transient failures | |
| "retry": { | |
| // Example: Maximum retry attempts (1 = no retry, 2 = one retry, etc.) |
Summary
stepOptionsfield to probitas.json for global default step optionsWhy
Previously, users had to specify step options (timeout, retry) on every step or scenario, leading to repetition. This change allows setting defaults in probitas.json, reducing boilerplate while maintaining the ability to override at scenario or step level.
The priority chain ensures flexibility: step options > scenario.stepOptions > config.stepOptions > builder defaults.
Implementation uses heuristic comparison against DEFAULT_* constants in Runner's StepRunner, since Builder resolves all undefined values to concrete defaults before execution.
Test Plan