cmdx 1.0.1 → 1.1.1
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.
- checksums.yaml +4 -4
- data/.cursor/prompts/docs.md +9 -0
- data/.cursor/prompts/rspec.md +21 -0
- data/.cursor/prompts/yardoc.md +13 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +29 -3
- data/README.md +2 -1
- data/docs/ai_prompts.md +269 -195
- data/docs/basics/call.md +126 -60
- data/docs/basics/chain.md +190 -160
- data/docs/basics/context.md +242 -154
- data/docs/basics/setup.md +302 -32
- data/docs/callbacks.md +382 -119
- data/docs/configuration.md +211 -49
- data/docs/deprecation.md +245 -0
- data/docs/getting_started.md +161 -39
- data/docs/internationalization.md +590 -70
- data/docs/interruptions/exceptions.md +135 -118
- data/docs/interruptions/faults.md +152 -127
- data/docs/interruptions/halt.md +134 -80
- data/docs/logging.md +183 -120
- data/docs/middlewares.md +165 -392
- data/docs/outcomes/result.md +140 -112
- data/docs/outcomes/states.md +134 -99
- data/docs/outcomes/statuses.md +204 -146
- data/docs/parameters/coercions.md +251 -289
- data/docs/parameters/defaults.md +224 -169
- data/docs/parameters/definitions.md +289 -141
- data/docs/parameters/namespacing.md +250 -161
- data/docs/parameters/validations.md +247 -159
- data/docs/testing.md +196 -203
- data/docs/workflows.md +146 -101
- data/lib/cmdx/.DS_Store +0 -0
- data/lib/cmdx/callback.rb +39 -55
- data/lib/cmdx/callback_registry.rb +80 -73
- data/lib/cmdx/chain.rb +65 -122
- data/lib/cmdx/chain_inspector.rb +23 -116
- data/lib/cmdx/chain_serializer.rb +34 -146
- data/lib/cmdx/coercion.rb +57 -0
- data/lib/cmdx/coercion_registry.rb +113 -0
- data/lib/cmdx/coercions/array.rb +18 -36
- data/lib/cmdx/coercions/big_decimal.rb +21 -33
- data/lib/cmdx/coercions/boolean.rb +21 -40
- data/lib/cmdx/coercions/complex.rb +18 -31
- data/lib/cmdx/coercions/date.rb +20 -39
- data/lib/cmdx/coercions/date_time.rb +22 -39
- data/lib/cmdx/coercions/float.rb +19 -32
- data/lib/cmdx/coercions/hash.rb +22 -41
- data/lib/cmdx/coercions/integer.rb +20 -33
- data/lib/cmdx/coercions/rational.rb +20 -32
- data/lib/cmdx/coercions/string.rb +23 -31
- data/lib/cmdx/coercions/time.rb +24 -40
- data/lib/cmdx/coercions/virtual.rb +14 -31
- data/lib/cmdx/configuration.rb +101 -162
- data/lib/cmdx/context.rb +34 -166
- data/lib/cmdx/core_ext/hash.rb +42 -67
- data/lib/cmdx/core_ext/module.rb +35 -79
- data/lib/cmdx/core_ext/object.rb +63 -98
- data/lib/cmdx/correlator.rb +59 -154
- data/lib/cmdx/error.rb +37 -202
- data/lib/cmdx/errors.rb +153 -216
- data/lib/cmdx/fault.rb +68 -150
- data/lib/cmdx/faults.rb +26 -137
- data/lib/cmdx/immutator.rb +22 -110
- data/lib/cmdx/lazy_struct.rb +110 -186
- data/lib/cmdx/log_formatters/json.rb +14 -40
- data/lib/cmdx/log_formatters/key_value.rb +14 -40
- data/lib/cmdx/log_formatters/line.rb +14 -48
- data/lib/cmdx/log_formatters/logstash.rb +14 -57
- data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
- data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
- data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
- data/lib/cmdx/log_formatters/raw.rb +19 -49
- data/lib/cmdx/logger.rb +22 -79
- data/lib/cmdx/logger_ansi.rb +31 -72
- data/lib/cmdx/logger_serializer.rb +74 -103
- data/lib/cmdx/middleware.rb +56 -60
- data/lib/cmdx/middleware_registry.rb +82 -77
- data/lib/cmdx/middlewares/correlate.rb +41 -226
- data/lib/cmdx/middlewares/timeout.rb +46 -185
- data/lib/cmdx/parameter.rb +167 -183
- data/lib/cmdx/parameter_evaluator.rb +231 -0
- data/lib/cmdx/parameter_inspector.rb +37 -55
- data/lib/cmdx/parameter_registry.rb +65 -84
- data/lib/cmdx/parameter_serializer.rb +32 -76
- data/lib/cmdx/railtie.rb +24 -107
- data/lib/cmdx/result.rb +254 -259
- data/lib/cmdx/result_ansi.rb +28 -80
- data/lib/cmdx/result_inspector.rb +34 -70
- data/lib/cmdx/result_logger.rb +23 -77
- data/lib/cmdx/result_serializer.rb +59 -125
- data/lib/cmdx/rspec/matchers.rb +28 -0
- data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
- data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
- data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
- data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
- data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
- data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
- data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
- data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
- data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
- data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
- data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
- data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
- data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
- data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
- data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
- data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
- data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
- data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
- data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
- data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
- data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
- data/lib/cmdx/task.rb +336 -427
- data/lib/cmdx/task_deprecator.rb +52 -0
- data/lib/cmdx/task_processor.rb +246 -0
- data/lib/cmdx/task_serializer.rb +34 -69
- data/lib/cmdx/utils/ansi_color.rb +13 -89
- data/lib/cmdx/utils/log_timestamp.rb +13 -42
- data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
- data/lib/cmdx/utils/name_affix.rb +21 -71
- data/lib/cmdx/validator.rb +57 -0
- data/lib/cmdx/validator_registry.rb +108 -0
- data/lib/cmdx/validators/exclusion.rb +55 -94
- data/lib/cmdx/validators/format.rb +31 -85
- data/lib/cmdx/validators/inclusion.rb +65 -110
- data/lib/cmdx/validators/length.rb +117 -133
- data/lib/cmdx/validators/numeric.rb +123 -130
- data/lib/cmdx/validators/presence.rb +38 -79
- data/lib/cmdx/version.rb +1 -7
- data/lib/cmdx/workflow.rb +58 -330
- data/lib/cmdx.rb +1 -1
- data/lib/generators/cmdx/install_generator.rb +14 -31
- data/lib/generators/cmdx/task_generator.rb +39 -55
- data/lib/generators/cmdx/templates/install.rb +24 -6
- data/lib/generators/cmdx/workflow_generator.rb +41 -66
- data/lib/locales/ar.yml +0 -1
- data/lib/locales/cs.yml +0 -1
- data/lib/locales/da.yml +0 -1
- data/lib/locales/de.yml +0 -1
- data/lib/locales/el.yml +0 -1
- data/lib/locales/en.yml +0 -1
- data/lib/locales/es.yml +0 -1
- data/lib/locales/fi.yml +0 -1
- data/lib/locales/fr.yml +0 -1
- data/lib/locales/he.yml +0 -1
- data/lib/locales/hi.yml +0 -1
- data/lib/locales/it.yml +0 -1
- data/lib/locales/ja.yml +0 -1
- data/lib/locales/ko.yml +0 -1
- data/lib/locales/nl.yml +0 -1
- data/lib/locales/no.yml +0 -1
- data/lib/locales/pl.yml +0 -1
- data/lib/locales/pt.yml +0 -1
- data/lib/locales/ru.yml +0 -1
- data/lib/locales/sv.yml +0 -1
- data/lib/locales/th.yml +0 -1
- data/lib/locales/tr.yml +0 -1
- data/lib/locales/vi.yml +0 -1
- data/lib/locales/zh.yml +0 -1
- metadata +36 -8
- data/lib/cmdx/parameter_validator.rb +0 -81
- data/lib/cmdx/parameter_value.rb +0 -244
- data/lib/cmdx/parameters_inspector.rb +0 -72
- data/lib/cmdx/parameters_serializer.rb +0 -115
- data/lib/cmdx/rspec/result_matchers.rb +0 -917
- data/lib/cmdx/rspec/task_matchers.rb +0 -570
- data/lib/cmdx/validators/custom.rb +0 -102
data/docs/testing.md
CHANGED
@@ -10,50 +10,54 @@ CMDx provides a comprehensive suite of custom RSpec matchers designed for expres
|
|
10
10
|
- [Result Matchers](#result-matchers)
|
11
11
|
- [Primary Outcome Matchers](#primary-outcome-matchers)
|
12
12
|
- [State and Status Matchers](#state-and-status-matchers)
|
13
|
+
- [Execution and Outcome Matchers](#execution-and-outcome-matchers)
|
13
14
|
- [Metadata and Context Matchers](#metadata-and-context-matchers)
|
14
15
|
- [Failure Chain Matchers](#failure-chain-matchers)
|
15
16
|
- [Task Matchers](#task-matchers)
|
16
|
-
- [
|
17
|
-
- [
|
18
|
-
- [Exception Handling Matchers](#exception-handling-matchers)
|
17
|
+
- [Structure and Lifecycle Matchers](#structure-and-lifecycle-matchers)
|
18
|
+
- [Parameter Testing Matchers](#parameter-testing-matchers)
|
19
19
|
- [Callback and Middleware Matchers](#callback-and-middleware-matchers)
|
20
20
|
- [Configuration Matchers](#configuration-matchers)
|
21
21
|
- [Composable Testing](#composable-testing)
|
22
|
+
- [Error Handling](#error-handling)
|
22
23
|
- [Best Practices](#best-practices)
|
23
24
|
|
24
25
|
## TLDR
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
27
|
+
```ruby
|
28
|
+
# Setup - require in spec helper
|
29
|
+
require "cmdx/rspec/matchers"
|
30
|
+
|
31
|
+
# Result outcome matchers
|
32
|
+
expect(result).to be_successful_task(user_id: 123)
|
33
|
+
expect(result).to be_failed_task("validation_error").with_metadata(field: "email")
|
34
|
+
expect(result).to be_skipped_task.with_reason("already_processed")
|
35
|
+
|
36
|
+
# Task structure matchers
|
37
|
+
expect(MyTask).to be_well_formed_task
|
38
|
+
expect(MyTask).to have_parameter(:email).that_is_required.with_type(:string)
|
39
|
+
expect(MyTask).to have_callback(:before_execution)
|
40
|
+
```
|
32
41
|
|
33
42
|
## External Project Setup
|
34
43
|
|
35
|
-
To use CMDx's custom matchers in an external RSpec-based project update your `spec/spec_helper.rb` or `spec/rails_helper.rb`:
|
44
|
+
To use CMDx's custom matchers in an external RSpec-based project, update your `spec/spec_helper.rb` or `spec/rails_helper.rb`:
|
36
45
|
|
37
46
|
```ruby
|
38
|
-
require "cmdx/rspec/
|
39
|
-
require "cmdx/rspec/task_matchers"
|
47
|
+
require "cmdx/rspec/matchers"
|
40
48
|
```
|
41
49
|
|
42
50
|
## Matcher Organization
|
43
51
|
|
44
|
-
CMDx matchers are organized into two primary
|
52
|
+
CMDx matchers are organized into two primary categories with comprehensive YARD documentation:
|
45
53
|
|
46
|
-
|
|
47
|
-
|
48
|
-
|
|
49
|
-
|
|
54
|
+
| Category | Purpose | Matcher Count |
|
55
|
+
|----------|---------|---------------|
|
56
|
+
| **Result Matchers** | Task execution outcomes and side effects | 17 matchers |
|
57
|
+
| **Task Matchers** | Task behavior, validation, and lifecycle | 6 matchers |
|
50
58
|
|
51
|
-
|
52
|
-
|
53
|
-
- Multiple usage examples
|
54
|
-
- Return value specifications
|
55
|
-
- Negation examples
|
56
|
-
- Version information
|
59
|
+
> [!NOTE]
|
60
|
+
> All matchers include complete parameter descriptions, multiple usage examples, return value specifications, negation examples, and version information.
|
57
61
|
|
58
62
|
## Result Matchers
|
59
63
|
|
@@ -70,15 +74,19 @@ expect(result).to be_successful_task
|
|
70
74
|
# Successful task with context validation
|
71
75
|
expect(result).to be_successful_task(user_id: 123, processed: true)
|
72
76
|
|
73
|
-
#
|
74
|
-
expect(result).
|
77
|
+
# With RSpec matchers for flexible context validation
|
78
|
+
expect(result).to be_successful_task(
|
79
|
+
user_id: be_a(Integer),
|
80
|
+
processed_at: be_a(Time),
|
81
|
+
email: match(/@/)
|
82
|
+
)
|
75
83
|
```
|
76
84
|
|
77
85
|
**What it validates:**
|
78
86
|
- Result has success status
|
79
87
|
- Result is in complete state
|
80
88
|
- Result was executed
|
81
|
-
- Optional context attributes match
|
89
|
+
- Optional context attributes match expected values
|
82
90
|
|
83
91
|
#### Failed Task Validation
|
84
92
|
|
@@ -87,15 +95,14 @@ expect(result).not_to be_successful_task
|
|
87
95
|
expect(result).to be_failed_task
|
88
96
|
|
89
97
|
# Failed task with specific reason
|
90
|
-
expect(result).to be_failed_task("
|
98
|
+
expect(result).to be_failed_task("validation_failed")
|
91
99
|
|
92
|
-
#
|
93
|
-
expect(result).to be_failed_task
|
94
|
-
.with_reason("Invalid data")
|
95
|
-
.with_metadata(error_code: "ERR001", retryable: false)
|
100
|
+
# Using with_reason chain
|
101
|
+
expect(result).to be_failed_task.with_reason("invalid_data")
|
96
102
|
|
97
|
-
#
|
98
|
-
expect(result).
|
103
|
+
# Combined reason and metadata validation
|
104
|
+
expect(result).to be_failed_task("validation_error")
|
105
|
+
.with_metadata(field: "email", rule: "format", retryable: false)
|
99
106
|
```
|
100
107
|
|
101
108
|
**What it validates:**
|
@@ -111,15 +118,14 @@ expect(result).not_to be_failed_task
|
|
111
118
|
expect(result).to be_skipped_task
|
112
119
|
|
113
120
|
# Skipped task with specific reason
|
114
|
-
expect(result).to be_skipped_task("
|
121
|
+
expect(result).to be_skipped_task("already_processed")
|
115
122
|
|
116
|
-
#
|
117
|
-
expect(result).to be_skipped_task
|
118
|
-
.with_reason("Order already processed")
|
119
|
-
.with_metadata(processed_at: be_a(Time), skip_code: "DUPLICATE")
|
123
|
+
# Using with_reason chain
|
124
|
+
expect(result).to be_skipped_task.with_reason("order_already_processed")
|
120
125
|
|
121
|
-
#
|
122
|
-
expect(result).
|
126
|
+
# Combined reason and metadata validation
|
127
|
+
expect(result).to be_skipped_task("data_unchanged")
|
128
|
+
.with_metadata(last_sync: be_a(Time), changes: 0)
|
123
129
|
```
|
124
130
|
|
125
131
|
**What it validates:**
|
@@ -135,29 +141,26 @@ Individual validation matchers for granular testing:
|
|
135
141
|
#### Execution State Matchers
|
136
142
|
|
137
143
|
```ruby
|
138
|
-
#
|
144
|
+
# Auto-generated from CMDx::Result::STATES
|
139
145
|
expect(result).to be_initialized
|
140
146
|
expect(result).to be_executing
|
141
147
|
expect(result).to be_complete
|
142
148
|
expect(result).to be_interrupted
|
143
|
-
|
144
|
-
# Negated usage
|
145
|
-
expect(result).not_to be_initialized
|
146
149
|
```
|
147
150
|
|
151
|
+
> [!IMPORTANT]
|
152
|
+
> State matchers are dynamically generated from the CMDx framework's state definitions, ensuring they stay in sync with framework updates.
|
153
|
+
|
148
154
|
#### Execution Status Matchers
|
149
155
|
|
150
156
|
```ruby
|
151
|
-
#
|
157
|
+
# Auto-generated from CMDx::Result::STATUSES
|
152
158
|
expect(result).to be_success
|
153
159
|
expect(result).to be_skipped
|
154
160
|
expect(result).to be_failed
|
155
|
-
|
156
|
-
# Negated usage
|
157
|
-
expect(result).not_to be_success
|
158
161
|
```
|
159
162
|
|
160
|
-
|
163
|
+
### Execution and Outcome Matchers
|
161
164
|
|
162
165
|
```ruby
|
163
166
|
# Execution validation
|
@@ -165,11 +168,7 @@ expect(result).to be_executed
|
|
165
168
|
|
166
169
|
# Outcome classification
|
167
170
|
expect(result).to have_good_outcome # success OR skipped
|
168
|
-
expect(result).to have_bad_outcome # not success
|
169
|
-
|
170
|
-
# Negated usage
|
171
|
-
expect(result).not_to be_executed
|
172
|
-
expect(result).not_to have_good_outcome
|
171
|
+
expect(result).to have_bad_outcome # failed (not success)
|
173
172
|
```
|
174
173
|
|
175
174
|
### Metadata and Context Matchers
|
@@ -177,25 +176,23 @@ expect(result).not_to have_good_outcome
|
|
177
176
|
#### Metadata Validation
|
178
177
|
|
179
178
|
```ruby
|
180
|
-
# Basic metadata validation
|
181
|
-
expect(result).to have_metadata(reason: "
|
179
|
+
# Basic metadata validation
|
180
|
+
expect(result).to have_metadata(reason: "validation_failed", code: 422)
|
181
|
+
|
182
|
+
# With RSpec matchers for flexible assertions
|
182
183
|
expect(result).to have_metadata(
|
183
|
-
reason: "
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
failed_at: be_a(Time)
|
184
|
+
reason: "validation_failed",
|
185
|
+
started_at: be_a(Time),
|
186
|
+
duration: be > 0,
|
187
|
+
error_code: match(/^ERR/)
|
188
188
|
)
|
189
189
|
|
190
190
|
# Chainable metadata inclusion
|
191
|
-
expect(result).to have_metadata(reason: "
|
192
|
-
.including(
|
191
|
+
expect(result).to have_metadata(reason: "error")
|
192
|
+
.including(retry_count: 3, retryable: false)
|
193
193
|
|
194
194
|
# Empty metadata validation
|
195
195
|
expect(result).to have_empty_metadata
|
196
|
-
|
197
|
-
# Negated usage
|
198
|
-
expect(result).not_to have_metadata(reason: "Different error")
|
199
196
|
```
|
200
197
|
|
201
198
|
#### Runtime Validation
|
@@ -210,51 +207,43 @@ expect(result).to have_runtime(0.5)
|
|
210
207
|
# Runtime with RSpec matchers
|
211
208
|
expect(result).to have_runtime(be > 0)
|
212
209
|
expect(result).to have_runtime(be_within(0.1).of(0.5))
|
213
|
-
|
214
|
-
# Negated usage
|
215
|
-
expect(result).not_to have_runtime
|
210
|
+
expect(result).to have_runtime(be < 2.0) # Performance constraint
|
216
211
|
```
|
217
212
|
|
218
213
|
#### Context Side Effects
|
219
214
|
|
220
215
|
```ruby
|
221
|
-
# Context validation with
|
216
|
+
# Context validation with direct values
|
222
217
|
expect(result).to have_context(processed: true, user_id: 123)
|
223
|
-
expect(result).to have_context(
|
224
|
-
processed_at: be_a(Time),
|
225
|
-
errors: be_empty,
|
226
|
-
count: be > 0
|
227
|
-
)
|
228
218
|
|
229
|
-
#
|
219
|
+
# With RSpec matchers for flexible validation
|
230
220
|
expect(result).to have_context(
|
231
221
|
user: have_attributes(id: 123, name: "John"),
|
222
|
+
processed_at: be_a(Time),
|
232
223
|
notifications: contain_exactly("email", "sms")
|
233
224
|
)
|
234
225
|
|
235
|
-
# Context preservation
|
236
|
-
expect(result).to
|
237
|
-
|
238
|
-
|
239
|
-
|
226
|
+
# Context preservation testing
|
227
|
+
expect(result).to have_preserved_context(
|
228
|
+
user_id: 123,
|
229
|
+
original_data: "important"
|
230
|
+
)
|
240
231
|
```
|
241
232
|
|
233
|
+
> [!TIP]
|
234
|
+
> Use `have_context` for testing side effects and new values, and `have_preserved_context` for verifying that certain values remained unchanged throughout execution.
|
235
|
+
|
242
236
|
#### Chain Validation
|
243
237
|
|
244
238
|
```ruby
|
245
|
-
# Basic chain membership validation
|
246
|
-
expect(result).to belong_to_chain
|
247
|
-
|
248
|
-
# Specific chain validation
|
249
|
-
expect(result).to belong_to_chain(my_chain)
|
250
|
-
|
251
239
|
# Chain position validation
|
252
240
|
expect(result).to have_chain_index(0) # First task in chain
|
253
241
|
expect(result).to have_chain_index(2) # Third task in chain
|
254
242
|
|
255
|
-
#
|
256
|
-
|
257
|
-
|
243
|
+
# Workflow structure testing
|
244
|
+
workflow_result = MyWorkflow.call(data: "test")
|
245
|
+
first_task = workflow_result.chain.first
|
246
|
+
expect(first_task).to have_chain_index(0)
|
258
247
|
```
|
259
248
|
|
260
249
|
### Failure Chain Matchers
|
@@ -267,8 +256,10 @@ Test CMDx's failure propagation patterns:
|
|
267
256
|
# Test that result represents an original failure (not propagated)
|
268
257
|
expect(result).to have_caused_failure
|
269
258
|
|
270
|
-
#
|
271
|
-
|
259
|
+
# Distinguished from thrown failures
|
260
|
+
result = ValidateDataTask.call(data: "invalid")
|
261
|
+
expect(result).to have_caused_failure
|
262
|
+
expect(result).not_to have_thrown_failure
|
272
263
|
```
|
273
264
|
|
274
265
|
#### Failure Propagation Validation
|
@@ -278,10 +269,10 @@ expect(result).not_to have_caused_failure
|
|
278
269
|
expect(result).to have_thrown_failure
|
279
270
|
|
280
271
|
# Thrown failure with specific original result
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
expect(
|
272
|
+
workflow_result = MultiStepWorkflow.call(data: "problematic")
|
273
|
+
original_failure = workflow_result.chain.find(&:caused_failure?)
|
274
|
+
throwing_task = workflow_result.chain.find(&:threw_failure?)
|
275
|
+
expect(throwing_task).to have_thrown_failure(original_failure)
|
285
276
|
```
|
286
277
|
|
287
278
|
#### Received Failure Validation
|
@@ -290,68 +281,25 @@ expect(result).not_to have_thrown_failure
|
|
290
281
|
# Test that result received a thrown failure
|
291
282
|
expect(result).to have_received_thrown_failure
|
292
283
|
|
293
|
-
#
|
294
|
-
|
284
|
+
# Testing downstream task failure handling
|
285
|
+
workflow_result = ProcessingWorkflow.call(data: "invalid")
|
286
|
+
receiving_task = workflow_result.chain.find { |r| r.thrown_failure? }
|
287
|
+
expect(receiving_task).to have_received_thrown_failure
|
295
288
|
```
|
296
289
|
|
297
290
|
## Task Matchers
|
298
291
|
|
299
|
-
###
|
300
|
-
|
301
|
-
Test task parameter validation behavior:
|
302
|
-
|
303
|
-
#### Required Parameter Validation
|
304
|
-
|
305
|
-
```ruby
|
306
|
-
# Test that task validates required parameters
|
307
|
-
expect(CreateUserTask).to validate_required_parameter(:email)
|
308
|
-
expect(ProcessOrderTask).to validate_required_parameter(:order_id)
|
309
|
-
|
310
|
-
# Negated usage
|
311
|
-
expect(OptionalTask).not_to validate_required_parameter(:optional_field)
|
312
|
-
```
|
313
|
-
|
314
|
-
**How it works:** Calls the task without the parameter and ensures it fails with appropriate validation message.
|
315
|
-
|
316
|
-
#### Type Validation
|
317
|
-
|
318
|
-
```ruby
|
319
|
-
# Test parameter type coercion validation
|
320
|
-
expect(CreateUserTask).to validate_parameter_type(:age, :integer)
|
321
|
-
expect(UpdateSettingsTask).to validate_parameter_type(:enabled, :boolean)
|
322
|
-
expect(SearchTask).to validate_parameter_type(:filters, :hash)
|
323
|
-
|
324
|
-
# Negated usage
|
325
|
-
expect(FlexibleTask).not_to validate_parameter_type(:flexible_param, :string)
|
326
|
-
```
|
327
|
-
|
328
|
-
**How it works:** Passes invalid type values and ensures task fails with type validation message.
|
329
|
-
|
330
|
-
#### Default Value Testing
|
331
|
-
|
332
|
-
```ruby
|
333
|
-
# Test parameter default values
|
334
|
-
expect(ProcessTask).to use_default_value(:timeout, 30)
|
335
|
-
expect(EmailTask).to use_default_value(:priority, "normal")
|
336
|
-
expect(ConfigTask).to use_default_value(:enabled, true)
|
337
|
-
|
338
|
-
# Negated usage
|
339
|
-
expect(RequiredParamTask).not_to use_default_value(:required_field, nil)
|
340
|
-
```
|
341
|
-
|
342
|
-
**How it works:** Calls task without the parameter and verifies the expected default value appears in context.
|
343
|
-
|
344
|
-
### Lifecycle and Structure Matchers
|
292
|
+
### Structure and Lifecycle Matchers
|
345
293
|
|
346
294
|
#### Well-Formed Task Validation
|
347
295
|
|
348
296
|
```ruby
|
349
297
|
# Test task meets all structural requirements
|
350
298
|
expect(MyTask).to be_well_formed_task
|
351
|
-
expect(UserCreationTask).to be_well_formed_task
|
352
299
|
|
353
|
-
#
|
354
|
-
|
300
|
+
# For dynamically created tasks
|
301
|
+
task_class = Class.new(CMDx::Task) { def call; end }
|
302
|
+
expect(task_class).to be_well_formed_task
|
355
303
|
```
|
356
304
|
|
357
305
|
**What it validates:**
|
@@ -359,65 +307,60 @@ expect(BrokenTask).not_to be_well_formed_task
|
|
359
307
|
- Implements required call method
|
360
308
|
- Has properly initialized parameter, callback, and middleware registries
|
361
309
|
|
362
|
-
###
|
363
|
-
|
364
|
-
#### Graceful Exception Handling
|
365
|
-
|
366
|
-
```ruby
|
367
|
-
# Test task converts exceptions to failed results
|
368
|
-
expect(RobustTask).to handle_exceptions_gracefully
|
369
|
-
|
370
|
-
# Negated usage (for exception-propagating tasks)
|
371
|
-
expect(StrictTask).not_to handle_exceptions_gracefully
|
372
|
-
```
|
373
|
-
|
374
|
-
**How it works:** Injects exception-raising logic and verifies exceptions are caught and converted to failed results.
|
310
|
+
### Parameter Testing Matchers
|
375
311
|
|
376
|
-
####
|
312
|
+
#### Parameter Presence and Configuration
|
377
313
|
|
378
314
|
```ruby
|
379
|
-
#
|
380
|
-
expect(
|
381
|
-
|
382
|
-
#
|
383
|
-
expect(
|
315
|
+
# Basic parameter presence
|
316
|
+
expect(CreateUserTask).to have_parameter(:email)
|
317
|
+
|
318
|
+
# Parameter requirement validation
|
319
|
+
expect(ProcessOrderTask).to have_parameter(:order_id).that_is_required
|
320
|
+
expect(ConfigTask).to have_parameter(:timeout).that_is_optional
|
321
|
+
|
322
|
+
# Type coercion validation
|
323
|
+
expect(CreateUserTask).to have_parameter(:age).with_type(:integer)
|
324
|
+
expect(UpdateSettingsTask).to have_parameter(:enabled).with_coercion(:boolean)
|
325
|
+
|
326
|
+
# Default value testing
|
327
|
+
expect(ProcessTask).to have_parameter(:timeout).with_default(30)
|
328
|
+
expect(EmailTask).to have_parameter(:priority).with_default("normal")
|
329
|
+
|
330
|
+
# Validation rules testing
|
331
|
+
expect(UserTask).to have_parameter(:email)
|
332
|
+
.with_validations(:format, :presence)
|
333
|
+
.that_is_required
|
334
|
+
.with_type(:string)
|
384
335
|
```
|
385
336
|
|
386
|
-
|
337
|
+
> [!WARNING]
|
338
|
+
> Parameter validation matchers test the configuration of parameters, not their runtime behavior. Use result matchers to test parameter validation failures during execution.
|
387
339
|
|
388
340
|
### Callback and Middleware Matchers
|
389
341
|
|
390
342
|
#### Callback Registration Testing
|
391
343
|
|
392
344
|
```ruby
|
393
|
-
#
|
345
|
+
# Basic callback registration
|
394
346
|
expect(ValidatedTask).to have_callback(:before_validation)
|
395
347
|
expect(NotifiedTask).to have_callback(:on_success)
|
396
348
|
expect(CleanupTask).to have_callback(:after_execution)
|
397
349
|
|
398
|
-
#
|
350
|
+
# Callback with specific callable (if supported by implementation)
|
399
351
|
expect(CustomTask).to have_callback(:on_failure).with_callable(my_proc)
|
400
|
-
|
401
|
-
# Negated usage
|
402
|
-
expect(SimpleTask).not_to have_callback(:complex_callback)
|
403
352
|
```
|
404
353
|
|
405
354
|
#### Callback Execution Testing
|
406
355
|
|
407
356
|
```ruby
|
408
357
|
# Test callbacks execute during task lifecycle
|
409
|
-
expect(
|
410
|
-
expect(
|
411
|
-
|
412
|
-
# Single callback execution
|
413
|
-
expect(simple_task).to execute_callbacks(:on_success)
|
414
|
-
|
415
|
-
# Negated usage
|
416
|
-
expect(task).not_to execute_callbacks(:unused_callback)
|
358
|
+
expect(task_instance).to have_executed_callbacks(:before_validation, :after_validation)
|
359
|
+
expect(failed_task_instance).to have_executed_callbacks(:before_execution, :on_failure)
|
417
360
|
```
|
418
361
|
|
419
362
|
> [!NOTE]
|
420
|
-
> Callback execution testing may require mocking internal callback mechanisms for comprehensive validation.
|
363
|
+
> Callback execution testing requires task instances rather than task classes and may require mocking internal callback mechanisms for comprehensive validation.
|
421
364
|
|
422
365
|
#### Middleware Registration Testing
|
423
366
|
|
@@ -426,9 +369,6 @@ expect(task).not_to execute_callbacks(:unused_callback)
|
|
426
369
|
expect(AuthenticatedTask).to have_middleware(AuthenticationMiddleware)
|
427
370
|
expect(LoggedTask).to have_middleware(LoggingMiddleware)
|
428
371
|
expect(TimedTask).to have_middleware(TimeoutMiddleware)
|
429
|
-
|
430
|
-
# Negated usage
|
431
|
-
expect(SimpleTask).not_to have_middleware(ComplexMiddleware)
|
432
372
|
```
|
433
373
|
|
434
374
|
### Configuration Matchers
|
@@ -437,15 +377,12 @@ expect(SimpleTask).not_to have_middleware(ComplexMiddleware)
|
|
437
377
|
|
438
378
|
```ruby
|
439
379
|
# Test setting presence
|
440
|
-
expect(ConfiguredTask).to
|
441
|
-
expect(CustomTask).to
|
380
|
+
expect(ConfiguredTask).to have_cmd_setting(:timeout)
|
381
|
+
expect(CustomTask).to have_cmd_setting(:priority)
|
442
382
|
|
443
383
|
# Test setting with specific value
|
444
|
-
expect(TimedTask).to
|
445
|
-
expect(PriorityTask).to
|
446
|
-
|
447
|
-
# Negated usage
|
448
|
-
expect(SimpleTask).not_to have_task_setting(:complex_setting)
|
384
|
+
expect(TimedTask).to have_cmd_setting(:timeout, 30)
|
385
|
+
expect(PriorityTask).to have_cmd_setting(:priority, "high")
|
449
386
|
```
|
450
387
|
|
451
388
|
## Composable Testing
|
@@ -459,13 +396,12 @@ Following RSpec best practices, CMDx matchers are designed for composition:
|
|
459
396
|
expect(result).to be_successful_task(user_id: 123)
|
460
397
|
.and have_context(processed_at: be_a(Time))
|
461
398
|
.and have_runtime(be > 0)
|
462
|
-
.and
|
399
|
+
.and have_chain_index(0)
|
463
400
|
|
464
401
|
# Chain task validation expectations
|
465
402
|
expect(TaskClass).to be_well_formed_task
|
466
|
-
.and
|
403
|
+
.and have_parameter(:user_id).that_is_required
|
467
404
|
.and have_callback(:before_execution)
|
468
|
-
.and handle_exceptions_gracefully
|
469
405
|
```
|
470
406
|
|
471
407
|
### Integration with Built-in RSpec Matchers
|
@@ -473,10 +409,10 @@ expect(TaskClass).to be_well_formed_task
|
|
473
409
|
```ruby
|
474
410
|
# Combine with built-in matchers
|
475
411
|
expect(result).to be_failed_task
|
476
|
-
.with_metadata(error_code:
|
412
|
+
.with_metadata(error_code: match(/^ERR/), retryable: be_falsy)
|
477
413
|
.and have_caused_failure
|
478
414
|
|
479
|
-
#
|
415
|
+
# Complex context validation
|
480
416
|
expect(result).to be_successful_task
|
481
417
|
.and have_context(
|
482
418
|
user: have_attributes(id: be_a(Integer), email: match(/@/)),
|
@@ -485,6 +421,41 @@ expect(result).to be_successful_task
|
|
485
421
|
)
|
486
422
|
```
|
487
423
|
|
424
|
+
## Error Handling
|
425
|
+
|
426
|
+
### Invalid Matcher Usage
|
427
|
+
|
428
|
+
Common error scenarios and their resolution:
|
429
|
+
|
430
|
+
```ruby
|
431
|
+
# Parameter not found
|
432
|
+
expect(SimpleTask).to have_parameter(:nonexistent)
|
433
|
+
# → "expected task to have parameter nonexistent, but had parameters: []"
|
434
|
+
|
435
|
+
# Middleware not registered
|
436
|
+
expect(SimpleTask).to have_middleware(ComplexMiddleware)
|
437
|
+
# → "expected task to have middleware ComplexMiddleware, but had []"
|
438
|
+
|
439
|
+
# Context mismatch
|
440
|
+
expect(result).to have_context(user_id: 999)
|
441
|
+
# → "expected context to include {user_id: 999}, but user_id: expected 999, got 123"
|
442
|
+
```
|
443
|
+
|
444
|
+
### Test Failures and Debugging
|
445
|
+
|
446
|
+
```ruby
|
447
|
+
# Use descriptive failure messages for debugging
|
448
|
+
result = ProcessDataTask.call(data: "invalid")
|
449
|
+
expect(result).to be_successful_task
|
450
|
+
# → "expected result to be successful, but was failed,
|
451
|
+
# expected result to be complete, but was interrupted"
|
452
|
+
|
453
|
+
# Combine matchers for comprehensive validation
|
454
|
+
expect(result).to be_failed_task("validation_error")
|
455
|
+
.with_metadata(field: "email", rule: "format")
|
456
|
+
# → Clear indication of what specifically failed
|
457
|
+
```
|
458
|
+
|
488
459
|
## Best Practices
|
489
460
|
|
490
461
|
### 1. Use Composite Matchers When Possible
|
@@ -496,7 +467,6 @@ expect(result).to be_successful_task(user_id: 123)
|
|
496
467
|
|
497
468
|
**Instead of:**
|
498
469
|
```ruby
|
499
|
-
expect(result).to be_a(CMDx::Result)
|
500
470
|
expect(result).to be_success
|
501
471
|
expect(result).to be_complete
|
502
472
|
expect(result).to be_executed
|
@@ -511,7 +481,7 @@ Use composite matchers for primary assertions, granular matchers for specific ed
|
|
511
481
|
# Primary assertion
|
512
482
|
expect(result).to be_successful_task
|
513
483
|
|
514
|
-
# Specific
|
484
|
+
# Specific validations
|
515
485
|
expect(result).to have_runtime(be < 1.0) # Performance requirement
|
516
486
|
expect(result).to have_chain_index(0) # Position validation
|
517
487
|
```
|
@@ -534,12 +504,12 @@ Matcher names are designed to read naturally in test descriptions:
|
|
534
504
|
|
535
505
|
```ruby
|
536
506
|
describe ProcessOrderTask do
|
537
|
-
it "
|
538
|
-
expect(described_class).to
|
507
|
+
it "has required parameters configured" do
|
508
|
+
expect(described_class).to have_parameter(:order_id).that_is_required
|
539
509
|
end
|
540
510
|
|
541
|
-
it "
|
542
|
-
expect(described_class).to
|
511
|
+
it "registers necessary callbacks" do
|
512
|
+
expect(described_class).to have_callback(:before_execution)
|
543
513
|
end
|
544
514
|
|
545
515
|
context "when processing succeeds" do
|
@@ -551,10 +521,33 @@ describe ProcessOrderTask do
|
|
551
521
|
.and have_runtime(be_positive)
|
552
522
|
end
|
553
523
|
end
|
524
|
+
|
525
|
+
context "when validation fails" do
|
526
|
+
it "returns failed result with error details" do
|
527
|
+
result = described_class.call(order_id: nil)
|
528
|
+
|
529
|
+
expect(result).to be_failed_task("validation_failed")
|
530
|
+
.with_metadata(field: "order_id", rule: "presence")
|
531
|
+
end
|
532
|
+
end
|
554
533
|
end
|
555
534
|
```
|
556
535
|
|
536
|
+
### 5. Test Both Happy and Error Paths
|
537
|
+
|
538
|
+
```ruby
|
539
|
+
# Happy path
|
540
|
+
expect(result).to be_successful_task
|
541
|
+
.and have_good_outcome
|
542
|
+
.and have_empty_metadata
|
543
|
+
|
544
|
+
# Error path
|
545
|
+
expect(error_result).to be_failed_task
|
546
|
+
.and have_bad_outcome
|
547
|
+
.and have_metadata(error_code: be_present)
|
548
|
+
```
|
549
|
+
|
557
550
|
---
|
558
551
|
|
559
552
|
- **Prev:** [Internationalization (i18n)](internationalization.md)
|
560
|
-
- **Next:** [
|
553
|
+
- **Next:** [Deprecation](deprecation.md)
|