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.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +21 -0
  4. data/.cursor/prompts/yardoc.md +13 -0
  5. data/.rubocop.yml +2 -0
  6. data/CHANGELOG.md +29 -3
  7. data/README.md +2 -1
  8. data/docs/ai_prompts.md +269 -195
  9. data/docs/basics/call.md +126 -60
  10. data/docs/basics/chain.md +190 -160
  11. data/docs/basics/context.md +242 -154
  12. data/docs/basics/setup.md +302 -32
  13. data/docs/callbacks.md +382 -119
  14. data/docs/configuration.md +211 -49
  15. data/docs/deprecation.md +245 -0
  16. data/docs/getting_started.md +161 -39
  17. data/docs/internationalization.md +590 -70
  18. data/docs/interruptions/exceptions.md +135 -118
  19. data/docs/interruptions/faults.md +152 -127
  20. data/docs/interruptions/halt.md +134 -80
  21. data/docs/logging.md +183 -120
  22. data/docs/middlewares.md +165 -392
  23. data/docs/outcomes/result.md +140 -112
  24. data/docs/outcomes/states.md +134 -99
  25. data/docs/outcomes/statuses.md +204 -146
  26. data/docs/parameters/coercions.md +251 -289
  27. data/docs/parameters/defaults.md +224 -169
  28. data/docs/parameters/definitions.md +289 -141
  29. data/docs/parameters/namespacing.md +250 -161
  30. data/docs/parameters/validations.md +247 -159
  31. data/docs/testing.md +196 -203
  32. data/docs/workflows.md +146 -101
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +39 -55
  35. data/lib/cmdx/callback_registry.rb +80 -73
  36. data/lib/cmdx/chain.rb +65 -122
  37. data/lib/cmdx/chain_inspector.rb +23 -116
  38. data/lib/cmdx/chain_serializer.rb +34 -146
  39. data/lib/cmdx/coercion.rb +57 -0
  40. data/lib/cmdx/coercion_registry.rb +113 -0
  41. data/lib/cmdx/coercions/array.rb +18 -36
  42. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  43. data/lib/cmdx/coercions/boolean.rb +21 -40
  44. data/lib/cmdx/coercions/complex.rb +18 -31
  45. data/lib/cmdx/coercions/date.rb +20 -39
  46. data/lib/cmdx/coercions/date_time.rb +22 -39
  47. data/lib/cmdx/coercions/float.rb +19 -32
  48. data/lib/cmdx/coercions/hash.rb +22 -41
  49. data/lib/cmdx/coercions/integer.rb +20 -33
  50. data/lib/cmdx/coercions/rational.rb +20 -32
  51. data/lib/cmdx/coercions/string.rb +23 -31
  52. data/lib/cmdx/coercions/time.rb +24 -40
  53. data/lib/cmdx/coercions/virtual.rb +14 -31
  54. data/lib/cmdx/configuration.rb +101 -162
  55. data/lib/cmdx/context.rb +34 -166
  56. data/lib/cmdx/core_ext/hash.rb +42 -67
  57. data/lib/cmdx/core_ext/module.rb +35 -79
  58. data/lib/cmdx/core_ext/object.rb +63 -98
  59. data/lib/cmdx/correlator.rb +59 -154
  60. data/lib/cmdx/error.rb +37 -202
  61. data/lib/cmdx/errors.rb +153 -216
  62. data/lib/cmdx/fault.rb +68 -150
  63. data/lib/cmdx/faults.rb +26 -137
  64. data/lib/cmdx/immutator.rb +22 -110
  65. data/lib/cmdx/lazy_struct.rb +110 -186
  66. data/lib/cmdx/log_formatters/json.rb +14 -40
  67. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  68. data/lib/cmdx/log_formatters/line.rb +14 -48
  69. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  70. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  71. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  72. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  73. data/lib/cmdx/log_formatters/raw.rb +19 -49
  74. data/lib/cmdx/logger.rb +22 -79
  75. data/lib/cmdx/logger_ansi.rb +31 -72
  76. data/lib/cmdx/logger_serializer.rb +74 -103
  77. data/lib/cmdx/middleware.rb +56 -60
  78. data/lib/cmdx/middleware_registry.rb +82 -77
  79. data/lib/cmdx/middlewares/correlate.rb +41 -226
  80. data/lib/cmdx/middlewares/timeout.rb +46 -185
  81. data/lib/cmdx/parameter.rb +167 -183
  82. data/lib/cmdx/parameter_evaluator.rb +231 -0
  83. data/lib/cmdx/parameter_inspector.rb +37 -55
  84. data/lib/cmdx/parameter_registry.rb +65 -84
  85. data/lib/cmdx/parameter_serializer.rb +32 -76
  86. data/lib/cmdx/railtie.rb +24 -107
  87. data/lib/cmdx/result.rb +254 -259
  88. data/lib/cmdx/result_ansi.rb +28 -80
  89. data/lib/cmdx/result_inspector.rb +34 -70
  90. data/lib/cmdx/result_logger.rb +23 -77
  91. data/lib/cmdx/result_serializer.rb +59 -125
  92. data/lib/cmdx/rspec/matchers.rb +28 -0
  93. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  94. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  96. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  97. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  98. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  99. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  100. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  101. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  102. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  103. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  104. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  105. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  106. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  107. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  108. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  109. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  110. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  111. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  112. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  113. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  114. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  115. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  116. data/lib/cmdx/task.rb +336 -427
  117. data/lib/cmdx/task_deprecator.rb +52 -0
  118. data/lib/cmdx/task_processor.rb +246 -0
  119. data/lib/cmdx/task_serializer.rb +34 -69
  120. data/lib/cmdx/utils/ansi_color.rb +13 -89
  121. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  122. data/lib/cmdx/utils/monotonic_runtime.rb +11 -63
  123. data/lib/cmdx/utils/name_affix.rb +21 -71
  124. data/lib/cmdx/validator.rb +57 -0
  125. data/lib/cmdx/validator_registry.rb +108 -0
  126. data/lib/cmdx/validators/exclusion.rb +55 -94
  127. data/lib/cmdx/validators/format.rb +31 -85
  128. data/lib/cmdx/validators/inclusion.rb +65 -110
  129. data/lib/cmdx/validators/length.rb +117 -133
  130. data/lib/cmdx/validators/numeric.rb +123 -130
  131. data/lib/cmdx/validators/presence.rb +38 -79
  132. data/lib/cmdx/version.rb +1 -7
  133. data/lib/cmdx/workflow.rb +58 -330
  134. data/lib/cmdx.rb +1 -1
  135. data/lib/generators/cmdx/install_generator.rb +14 -31
  136. data/lib/generators/cmdx/task_generator.rb +39 -55
  137. data/lib/generators/cmdx/templates/install.rb +24 -6
  138. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  139. data/lib/locales/ar.yml +0 -1
  140. data/lib/locales/cs.yml +0 -1
  141. data/lib/locales/da.yml +0 -1
  142. data/lib/locales/de.yml +0 -1
  143. data/lib/locales/el.yml +0 -1
  144. data/lib/locales/en.yml +0 -1
  145. data/lib/locales/es.yml +0 -1
  146. data/lib/locales/fi.yml +0 -1
  147. data/lib/locales/fr.yml +0 -1
  148. data/lib/locales/he.yml +0 -1
  149. data/lib/locales/hi.yml +0 -1
  150. data/lib/locales/it.yml +0 -1
  151. data/lib/locales/ja.yml +0 -1
  152. data/lib/locales/ko.yml +0 -1
  153. data/lib/locales/nl.yml +0 -1
  154. data/lib/locales/no.yml +0 -1
  155. data/lib/locales/pl.yml +0 -1
  156. data/lib/locales/pt.yml +0 -1
  157. data/lib/locales/ru.yml +0 -1
  158. data/lib/locales/sv.yml +0 -1
  159. data/lib/locales/th.yml +0 -1
  160. data/lib/locales/tr.yml +0 -1
  161. data/lib/locales/vi.yml +0 -1
  162. data/lib/locales/zh.yml +0 -1
  163. metadata +36 -8
  164. data/lib/cmdx/parameter_validator.rb +0 -81
  165. data/lib/cmdx/parameter_value.rb +0 -244
  166. data/lib/cmdx/parameters_inspector.rb +0 -72
  167. data/lib/cmdx/parameters_serializer.rb +0 -115
  168. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  169. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  170. 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
- - [Parameter Validation Matchers](#parameter-validation-matchers)
17
- - [Lifecycle and Structure Matchers](#lifecycle-and-structure-matchers)
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
- - **Custom matchers** - 40+ specialized RSpec matchers for testing CMDx tasks and results
27
- - **Setup** - Require `cmdx/rspec/result_matchers` and `cmdx/rspec/task_matchers`
28
- - **Result matchers** - `be_successful_task`, `be_failed_task`, `be_skipped_task` with chainable metadata
29
- - **Task matchers** - Parameter validation, lifecycle, exception handling, and configuration testing
30
- - **Composable** - Chain matchers for complex validation scenarios
31
- - **YARD documented** - Complete documentation with examples for all matchers
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/result_matchers"
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 files with comprehensive YARD documentation:
52
+ CMDx matchers are organized into two primary categories with comprehensive YARD documentation:
45
53
 
46
- | File | Purpose | Matcher Count |
47
- |------|---------|---------------|
48
- | `result_matchers.rb` | Task execution outcomes and side effects | 25+ matchers |
49
- | `task_matchers.rb` | Task behavior, validation, and lifecycle | 15+ matchers |
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
- All matchers include:
52
- - Complete parameter descriptions
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
- # Negated usage
74
- expect(result).not_to be_successful_task
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("Validation failed")
98
+ expect(result).to be_failed_task("validation_failed")
91
99
 
92
- # Chainable reason and metadata validation
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
- # Negated usage
98
- expect(result).not_to be_failed_task
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("Already processed")
121
+ expect(result).to be_skipped_task("already_processed")
115
122
 
116
- # Chainable reason and metadata validation
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
- # Negated usage
122
- expect(result).not_to be_skipped_task
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
- # Individual state checks (auto-generated from CMDx::Result::STATES)
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
- # Individual status checks (auto-generated from CMDx::Result::STATUSES)
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
- #### Execution and Outcome Matchers
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 (includes skipped and failed)
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 with RSpec matcher support
181
- expect(result).to have_metadata(reason: "Error", code: "001")
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: "Invalid email format",
184
- errors: ["Email must contain @"],
185
- error_code: "VALIDATION_FAILED",
186
- retryable: false,
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: "Error")
192
- .including(code: "001", retryable: false)
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 RSpec matcher support
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
- # Complex side effects validation
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 preserve_context(original_data)
237
-
238
- # Negated usage
239
- expect(result).not_to have_context(deleted: true)
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
- # Negated usage
256
- expect(result).not_to belong_to_chain
257
- expect(result).not_to have_chain_index(1)
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
- # Negated usage (for thrown failures)
271
- expect(result).not_to have_caused_failure
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
- expect(result).to have_thrown_failure(original_failed_result)
282
-
283
- # Negated usage (for caused failures)
284
- expect(result).not_to have_thrown_failure
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
- # Negated usage
294
- expect(result).not_to have_received_thrown_failure
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
- ### Parameter Validation Matchers
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
- # Negated usage (for malformed tasks)
354
- expect(BrokenTask).not_to be_well_formed_task
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
- ### Exception Handling Matchers
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
- #### Bang Method Exception Propagation
312
+ #### Parameter Presence and Configuration
377
313
 
378
314
  ```ruby
379
- # Test task propagates exceptions with call!
380
- expect(MyTask).to propagate_exceptions_with_bang
381
-
382
- # Negated usage (for always-graceful tasks)
383
- expect(AlwaysGracefulTask).not_to propagate_exceptions_with_bang
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
- **How it works:** Tests that `call!` method propagates exceptions instead of handling them gracefully.
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
- # Test basic callback registration
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
- # Test callback with specific callable
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(task).to execute_callbacks(:before_validation, :after_validation)
410
- expect(failed_task).to execute_callbacks(:before_execution, :on_failure)
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 have_task_setting(:timeout)
441
- expect(CustomTask).to have_task_setting(:priority)
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 have_task_setting(:timeout, 30)
445
- expect(PriorityTask).to have_task_setting(:priority, "high")
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 belong_to_chain
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 validate_required_parameter(:user_id)
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: "ERR001", retryable: be_falsy)
412
+ .with_metadata(error_code: match(/^ERR/), retryable: be_falsy)
477
413
  .and have_caused_failure
478
414
 
479
- # Use in complex scenarios
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 edge case validation
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 "validates required parameters" do
538
- expect(described_class).to validate_required_parameter(:order_id)
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 "handles exceptions gracefully" do
542
- expect(described_class).to handle_exceptions_gracefully
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:** [AI Prompts](ai_prompts.md)
553
+ - **Next:** [Deprecation](deprecation.md)