cmdx 1.1.0 → 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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/docs.md +9 -0
  3. data/.cursor/prompts/rspec.md +13 -12
  4. data/.cursor/prompts/yardoc.md +11 -6
  5. data/CHANGELOG.md +13 -2
  6. data/README.md +1 -0
  7. data/docs/ai_prompts.md +269 -195
  8. data/docs/basics/call.md +124 -58
  9. data/docs/basics/chain.md +190 -160
  10. data/docs/basics/context.md +242 -154
  11. data/docs/basics/setup.md +302 -32
  12. data/docs/callbacks.md +390 -94
  13. data/docs/configuration.md +181 -65
  14. data/docs/deprecation.md +245 -0
  15. data/docs/getting_started.md +161 -39
  16. data/docs/internationalization.md +590 -70
  17. data/docs/interruptions/exceptions.md +135 -118
  18. data/docs/interruptions/faults.md +150 -125
  19. data/docs/interruptions/halt.md +134 -80
  20. data/docs/logging.md +181 -118
  21. data/docs/middlewares.md +150 -377
  22. data/docs/outcomes/result.md +140 -112
  23. data/docs/outcomes/states.md +134 -99
  24. data/docs/outcomes/statuses.md +204 -146
  25. data/docs/parameters/coercions.md +232 -281
  26. data/docs/parameters/defaults.md +224 -169
  27. data/docs/parameters/definitions.md +289 -141
  28. data/docs/parameters/namespacing.md +250 -161
  29. data/docs/parameters/validations.md +260 -133
  30. data/docs/testing.md +191 -197
  31. data/docs/workflows.md +143 -98
  32. data/lib/cmdx/callback.rb +23 -19
  33. data/lib/cmdx/callback_registry.rb +1 -3
  34. data/lib/cmdx/chain_inspector.rb +23 -23
  35. data/lib/cmdx/chain_serializer.rb +38 -19
  36. data/lib/cmdx/coercion.rb +20 -12
  37. data/lib/cmdx/coercion_registry.rb +51 -32
  38. data/lib/cmdx/configuration.rb +84 -31
  39. data/lib/cmdx/context.rb +32 -21
  40. data/lib/cmdx/core_ext/hash.rb +13 -13
  41. data/lib/cmdx/core_ext/module.rb +1 -1
  42. data/lib/cmdx/core_ext/object.rb +12 -12
  43. data/lib/cmdx/correlator.rb +60 -39
  44. data/lib/cmdx/errors.rb +105 -131
  45. data/lib/cmdx/fault.rb +66 -45
  46. data/lib/cmdx/immutator.rb +20 -21
  47. data/lib/cmdx/lazy_struct.rb +78 -70
  48. data/lib/cmdx/log_formatters/json.rb +1 -1
  49. data/lib/cmdx/log_formatters/key_value.rb +1 -1
  50. data/lib/cmdx/log_formatters/line.rb +1 -1
  51. data/lib/cmdx/log_formatters/logstash.rb +1 -1
  52. data/lib/cmdx/log_formatters/pretty_json.rb +1 -1
  53. data/lib/cmdx/log_formatters/pretty_key_value.rb +1 -1
  54. data/lib/cmdx/log_formatters/pretty_line.rb +1 -1
  55. data/lib/cmdx/log_formatters/raw.rb +2 -2
  56. data/lib/cmdx/logger.rb +19 -14
  57. data/lib/cmdx/logger_ansi.rb +33 -17
  58. data/lib/cmdx/logger_serializer.rb +85 -24
  59. data/lib/cmdx/middleware.rb +39 -21
  60. data/lib/cmdx/middleware_registry.rb +4 -3
  61. data/lib/cmdx/parameter.rb +151 -89
  62. data/lib/cmdx/parameter_inspector.rb +34 -21
  63. data/lib/cmdx/parameter_registry.rb +36 -30
  64. data/lib/cmdx/parameter_serializer.rb +21 -14
  65. data/lib/cmdx/result.rb +136 -135
  66. data/lib/cmdx/result_ansi.rb +31 -17
  67. data/lib/cmdx/result_inspector.rb +32 -27
  68. data/lib/cmdx/result_logger.rb +23 -14
  69. data/lib/cmdx/result_serializer.rb +65 -27
  70. data/lib/cmdx/task.rb +234 -113
  71. data/lib/cmdx/task_deprecator.rb +22 -25
  72. data/lib/cmdx/task_processor.rb +89 -88
  73. data/lib/cmdx/task_serializer.rb +27 -14
  74. data/lib/cmdx/utils/monotonic_runtime.rb +2 -4
  75. data/lib/cmdx/validator.rb +25 -16
  76. data/lib/cmdx/validator_registry.rb +53 -31
  77. data/lib/cmdx/validators/exclusion.rb +1 -1
  78. data/lib/cmdx/validators/format.rb +2 -2
  79. data/lib/cmdx/validators/inclusion.rb +2 -2
  80. data/lib/cmdx/validators/length.rb +2 -2
  81. data/lib/cmdx/validators/numeric.rb +3 -3
  82. data/lib/cmdx/validators/presence.rb +2 -2
  83. data/lib/cmdx/version.rb +1 -1
  84. data/lib/cmdx/workflow.rb +54 -33
  85. data/lib/generators/cmdx/task_generator.rb +6 -6
  86. data/lib/generators/cmdx/workflow_generator.rb +6 -6
  87. metadata +3 -1
data/docs/testing.md CHANGED
@@ -10,29 +10,38 @@ 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/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
47
  require "cmdx/rspec/matchers"
@@ -40,19 +49,15 @@ require "cmdx/rspec/matchers"
40
49
 
41
50
  ## Matcher Organization
42
51
 
43
- 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:
44
53
 
45
- | Purpose | Matcher Count |
46
- |---------|---------------|
47
- | Task execution outcomes and side effects | 15+ matchers |
48
- | Task behavior, validation, and lifecycle | 5+ 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 |
49
58
 
50
- All matchers include:
51
- - Complete parameter descriptions
52
- - Multiple usage examples
53
- - Return value specifications
54
- - Negation examples
55
- - Version information
59
+ > [!NOTE]
60
+ > All matchers include complete parameter descriptions, multiple usage examples, return value specifications, negation examples, and version information.
56
61
 
57
62
  ## Result Matchers
58
63
 
@@ -69,15 +74,19 @@ expect(result).to be_successful_task
69
74
  # Successful task with context validation
70
75
  expect(result).to be_successful_task(user_id: 123, processed: true)
71
76
 
72
- # Negated usage
73
- 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
+ )
74
83
  ```
75
84
 
76
85
  **What it validates:**
77
86
  - Result has success status
78
87
  - Result is in complete state
79
88
  - Result was executed
80
- - Optional context attributes match
89
+ - Optional context attributes match expected values
81
90
 
82
91
  #### Failed Task Validation
83
92
 
@@ -86,15 +95,14 @@ expect(result).not_to be_successful_task
86
95
  expect(result).to be_failed_task
87
96
 
88
97
  # Failed task with specific reason
89
- expect(result).to be_failed_task("Validation failed")
98
+ expect(result).to be_failed_task("validation_failed")
90
99
 
91
- # Chainable reason and metadata validation
92
- expect(result).to be_failed_task
93
- .with_reason("Invalid data")
94
- .with_metadata(error_code: "ERR001", retryable: false)
100
+ # Using with_reason chain
101
+ expect(result).to be_failed_task.with_reason("invalid_data")
95
102
 
96
- # Negated usage
97
- 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)
98
106
  ```
99
107
 
100
108
  **What it validates:**
@@ -110,15 +118,14 @@ expect(result).not_to be_failed_task
110
118
  expect(result).to be_skipped_task
111
119
 
112
120
  # Skipped task with specific reason
113
- expect(result).to be_skipped_task("Already processed")
121
+ expect(result).to be_skipped_task("already_processed")
114
122
 
115
- # Chainable reason and metadata validation
116
- expect(result).to be_skipped_task
117
- .with_reason("Order already processed")
118
- .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")
119
125
 
120
- # Negated usage
121
- 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)
122
129
  ```
123
130
 
124
131
  **What it validates:**
@@ -134,29 +141,26 @@ Individual validation matchers for granular testing:
134
141
  #### Execution State Matchers
135
142
 
136
143
  ```ruby
137
- # Individual state checks (auto-generated from CMDx::Result::STATES)
144
+ # Auto-generated from CMDx::Result::STATES
138
145
  expect(result).to be_initialized
139
146
  expect(result).to be_executing
140
147
  expect(result).to be_complete
141
148
  expect(result).to be_interrupted
142
-
143
- # Negated usage
144
- expect(result).not_to be_initialized
145
149
  ```
146
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
+
147
154
  #### Execution Status Matchers
148
155
 
149
156
  ```ruby
150
- # Individual status checks (auto-generated from CMDx::Result::STATUSES)
157
+ # Auto-generated from CMDx::Result::STATUSES
151
158
  expect(result).to be_success
152
159
  expect(result).to be_skipped
153
160
  expect(result).to be_failed
154
-
155
- # Negated usage
156
- expect(result).not_to be_success
157
161
  ```
158
162
 
159
- #### Execution and Outcome Matchers
163
+ ### Execution and Outcome Matchers
160
164
 
161
165
  ```ruby
162
166
  # Execution validation
@@ -164,11 +168,7 @@ expect(result).to be_executed
164
168
 
165
169
  # Outcome classification
166
170
  expect(result).to have_good_outcome # success OR skipped
167
- expect(result).to have_bad_outcome # not success (includes skipped and failed)
168
-
169
- # Negated usage
170
- expect(result).not_to be_executed
171
- expect(result).not_to have_good_outcome
171
+ expect(result).to have_bad_outcome # failed (not success)
172
172
  ```
173
173
 
174
174
  ### Metadata and Context Matchers
@@ -176,25 +176,23 @@ expect(result).not_to have_good_outcome
176
176
  #### Metadata Validation
177
177
 
178
178
  ```ruby
179
- # Basic metadata validation with RSpec matcher support
180
- 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
181
183
  expect(result).to have_metadata(
182
- reason: "Invalid email format",
183
- errors: ["Email must contain @"],
184
- error_code: "VALIDATION_FAILED",
185
- retryable: false,
186
- failed_at: be_a(Time)
184
+ reason: "validation_failed",
185
+ started_at: be_a(Time),
186
+ duration: be > 0,
187
+ error_code: match(/^ERR/)
187
188
  )
188
189
 
189
190
  # Chainable metadata inclusion
190
- expect(result).to have_metadata(reason: "Error")
191
- .including(code: "001", retryable: false)
191
+ expect(result).to have_metadata(reason: "error")
192
+ .including(retry_count: 3, retryable: false)
192
193
 
193
194
  # Empty metadata validation
194
195
  expect(result).to have_empty_metadata
195
-
196
- # Negated usage
197
- expect(result).not_to have_metadata(reason: "Different error")
198
196
  ```
199
197
 
200
198
  #### Runtime Validation
@@ -209,51 +207,43 @@ expect(result).to have_runtime(0.5)
209
207
  # Runtime with RSpec matchers
210
208
  expect(result).to have_runtime(be > 0)
211
209
  expect(result).to have_runtime(be_within(0.1).of(0.5))
212
-
213
- # Negated usage
214
- expect(result).not_to have_runtime
210
+ expect(result).to have_runtime(be < 2.0) # Performance constraint
215
211
  ```
216
212
 
217
213
  #### Context Side Effects
218
214
 
219
215
  ```ruby
220
- # Context validation with RSpec matcher support
216
+ # Context validation with direct values
221
217
  expect(result).to have_context(processed: true, user_id: 123)
222
- expect(result).to have_context(
223
- processed_at: be_a(Time),
224
- errors: be_empty,
225
- count: be > 0
226
- )
227
218
 
228
- # Complex side effects validation
219
+ # With RSpec matchers for flexible validation
229
220
  expect(result).to have_context(
230
221
  user: have_attributes(id: 123, name: "John"),
222
+ processed_at: be_a(Time),
231
223
  notifications: contain_exactly("email", "sms")
232
224
  )
233
225
 
234
- # Context preservation
235
- expect(result).to preserve_context(original_data)
236
-
237
- # Negated usage
238
- 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
+ )
239
231
  ```
240
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
+
241
236
  #### Chain Validation
242
237
 
243
238
  ```ruby
244
- # Basic chain membership validation
245
- expect(result).to belong_to_chain
246
-
247
- # Specific chain validation
248
- expect(result).to belong_to_chain(my_chain)
249
-
250
239
  # Chain position validation
251
240
  expect(result).to have_chain_index(0) # First task in chain
252
241
  expect(result).to have_chain_index(2) # Third task in chain
253
242
 
254
- # Negated usage
255
- expect(result).not_to belong_to_chain
256
- 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)
257
247
  ```
258
248
 
259
249
  ### Failure Chain Matchers
@@ -266,8 +256,10 @@ Test CMDx's failure propagation patterns:
266
256
  # Test that result represents an original failure (not propagated)
267
257
  expect(result).to have_caused_failure
268
258
 
269
- # Negated usage (for thrown failures)
270
- 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
271
263
  ```
272
264
 
273
265
  #### Failure Propagation Validation
@@ -277,10 +269,10 @@ expect(result).not_to have_caused_failure
277
269
  expect(result).to have_thrown_failure
278
270
 
279
271
  # Thrown failure with specific original result
280
- expect(result).to have_thrown_failure(original_failed_result)
281
-
282
- # Negated usage (for caused failures)
283
- 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)
284
276
  ```
285
277
 
286
278
  #### Received Failure Validation
@@ -289,68 +281,25 @@ expect(result).not_to have_thrown_failure
289
281
  # Test that result received a thrown failure
290
282
  expect(result).to have_received_thrown_failure
291
283
 
292
- # Negated usage
293
- 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
294
288
  ```
295
289
 
296
290
  ## Task Matchers
297
291
 
298
- ### Parameter Validation Matchers
299
-
300
- Test task parameter validation behavior:
301
-
302
- #### Required Parameter Validation
303
-
304
- ```ruby
305
- # Test that task validates required parameters
306
- expect(CreateUserTask).to validate_required_parameter(:email)
307
- expect(ProcessOrderTask).to validate_required_parameter(:order_id)
308
-
309
- # Negated usage
310
- expect(OptionalTask).not_to validate_required_parameter(:optional_field)
311
- ```
312
-
313
- **How it works:** Calls the task without the parameter and ensures it fails with appropriate validation message.
314
-
315
- #### Type Validation
316
-
317
- ```ruby
318
- # Test parameter type coercion validation
319
- expect(CreateUserTask).to validate_parameter_type(:age, :integer)
320
- expect(UpdateSettingsTask).to validate_parameter_type(:enabled, :boolean)
321
- expect(SearchTask).to validate_parameter_type(:filters, :hash)
322
-
323
- # Negated usage
324
- expect(FlexibleTask).not_to validate_parameter_type(:flexible_param, :string)
325
- ```
326
-
327
- **How it works:** Passes invalid type values and ensures task fails with type validation message.
328
-
329
- #### Default Value Testing
330
-
331
- ```ruby
332
- # Test parameter default values
333
- expect(ProcessTask).to use_default_value(:timeout, 30)
334
- expect(EmailTask).to use_default_value(:priority, "normal")
335
- expect(ConfigTask).to use_default_value(:enabled, true)
336
-
337
- # Negated usage
338
- expect(RequiredParamTask).not_to use_default_value(:required_field, nil)
339
- ```
340
-
341
- **How it works:** Calls task without the parameter and verifies the expected default value appears in context.
342
-
343
- ### Lifecycle and Structure Matchers
292
+ ### Structure and Lifecycle Matchers
344
293
 
345
294
  #### Well-Formed Task Validation
346
295
 
347
296
  ```ruby
348
297
  # Test task meets all structural requirements
349
298
  expect(MyTask).to be_well_formed_task
350
- expect(UserCreationTask).to be_well_formed_task
351
299
 
352
- # Negated usage (for malformed tasks)
353
- 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
354
303
  ```
355
304
 
356
305
  **What it validates:**
@@ -358,65 +307,60 @@ expect(BrokenTask).not_to be_well_formed_task
358
307
  - Implements required call method
359
308
  - Has properly initialized parameter, callback, and middleware registries
360
309
 
361
- ### Exception Handling Matchers
362
-
363
- #### Graceful Exception Handling
364
-
365
- ```ruby
366
- # Test task converts exceptions to failed results
367
- expect(RobustTask).to handle_exceptions_gracefully
368
-
369
- # Negated usage (for exception-propagating tasks)
370
- expect(StrictTask).not_to handle_exceptions_gracefully
371
- ```
372
-
373
- **How it works:** Injects exception-raising logic and verifies exceptions are caught and converted to failed results.
310
+ ### Parameter Testing Matchers
374
311
 
375
- #### Bang Method Exception Propagation
312
+ #### Parameter Presence and Configuration
376
313
 
377
314
  ```ruby
378
- # Test task propagates exceptions with call!
379
- expect(MyTask).to propagate_exceptions_with_bang
380
-
381
- # Negated usage (for always-graceful tasks)
382
- 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)
383
335
  ```
384
336
 
385
- **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.
386
339
 
387
340
  ### Callback and Middleware Matchers
388
341
 
389
342
  #### Callback Registration Testing
390
343
 
391
344
  ```ruby
392
- # Test basic callback registration
345
+ # Basic callback registration
393
346
  expect(ValidatedTask).to have_callback(:before_validation)
394
347
  expect(NotifiedTask).to have_callback(:on_success)
395
348
  expect(CleanupTask).to have_callback(:after_execution)
396
349
 
397
- # Test callback with specific callable
350
+ # Callback with specific callable (if supported by implementation)
398
351
  expect(CustomTask).to have_callback(:on_failure).with_callable(my_proc)
399
-
400
- # Negated usage
401
- expect(SimpleTask).not_to have_callback(:complex_callback)
402
352
  ```
403
353
 
404
354
  #### Callback Execution Testing
405
355
 
406
356
  ```ruby
407
357
  # Test callbacks execute during task lifecycle
408
- expect(task).to execute_callbacks(:before_validation, :after_validation)
409
- expect(failed_task).to execute_callbacks(:before_execution, :on_failure)
410
-
411
- # Single callback execution
412
- expect(simple_task).to execute_callbacks(:on_success)
413
-
414
- # Negated usage
415
- 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)
416
360
  ```
417
361
 
418
362
  > [!NOTE]
419
- > 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.
420
364
 
421
365
  #### Middleware Registration Testing
422
366
 
@@ -425,9 +369,6 @@ expect(task).not_to execute_callbacks(:unused_callback)
425
369
  expect(AuthenticatedTask).to have_middleware(AuthenticationMiddleware)
426
370
  expect(LoggedTask).to have_middleware(LoggingMiddleware)
427
371
  expect(TimedTask).to have_middleware(TimeoutMiddleware)
428
-
429
- # Negated usage
430
- expect(SimpleTask).not_to have_middleware(ComplexMiddleware)
431
372
  ```
432
373
 
433
374
  ### Configuration Matchers
@@ -442,9 +383,6 @@ expect(CustomTask).to have_cmd_setting(:priority)
442
383
  # Test setting with specific value
443
384
  expect(TimedTask).to have_cmd_setting(:timeout, 30)
444
385
  expect(PriorityTask).to have_cmd_setting(:priority, "high")
445
-
446
- # Negated usage
447
- expect(SimpleTask).not_to have_cmd_setting(:complex_setting)
448
386
  ```
449
387
 
450
388
  ## Composable Testing
@@ -458,13 +396,12 @@ Following RSpec best practices, CMDx matchers are designed for composition:
458
396
  expect(result).to be_successful_task(user_id: 123)
459
397
  .and have_context(processed_at: be_a(Time))
460
398
  .and have_runtime(be > 0)
461
- .and belong_to_chain
399
+ .and have_chain_index(0)
462
400
 
463
401
  # Chain task validation expectations
464
402
  expect(TaskClass).to be_well_formed_task
465
- .and validate_required_parameter(:user_id)
403
+ .and have_parameter(:user_id).that_is_required
466
404
  .and have_callback(:before_execution)
467
- .and handle_exceptions_gracefully
468
405
  ```
469
406
 
470
407
  ### Integration with Built-in RSpec Matchers
@@ -472,10 +409,10 @@ expect(TaskClass).to be_well_formed_task
472
409
  ```ruby
473
410
  # Combine with built-in matchers
474
411
  expect(result).to be_failed_task
475
- .with_metadata(error_code: "ERR001", retryable: be_falsy)
412
+ .with_metadata(error_code: match(/^ERR/), retryable: be_falsy)
476
413
  .and have_caused_failure
477
414
 
478
- # Use in complex scenarios
415
+ # Complex context validation
479
416
  expect(result).to be_successful_task
480
417
  .and have_context(
481
418
  user: have_attributes(id: be_a(Integer), email: match(/@/)),
@@ -484,6 +421,41 @@ expect(result).to be_successful_task
484
421
  )
485
422
  ```
486
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
+
487
459
  ## Best Practices
488
460
 
489
461
  ### 1. Use Composite Matchers When Possible
@@ -495,7 +467,6 @@ expect(result).to be_successful_task(user_id: 123)
495
467
 
496
468
  **Instead of:**
497
469
  ```ruby
498
- expect(result).to be_a(CMDx::Result)
499
470
  expect(result).to be_success
500
471
  expect(result).to be_complete
501
472
  expect(result).to be_executed
@@ -510,7 +481,7 @@ Use composite matchers for primary assertions, granular matchers for specific ed
510
481
  # Primary assertion
511
482
  expect(result).to be_successful_task
512
483
 
513
- # Specific edge case validation
484
+ # Specific validations
514
485
  expect(result).to have_runtime(be < 1.0) # Performance requirement
515
486
  expect(result).to have_chain_index(0) # Position validation
516
487
  ```
@@ -533,12 +504,12 @@ Matcher names are designed to read naturally in test descriptions:
533
504
 
534
505
  ```ruby
535
506
  describe ProcessOrderTask do
536
- it "validates required parameters" do
537
- 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
538
509
  end
539
510
 
540
- it "handles exceptions gracefully" do
541
- expect(described_class).to handle_exceptions_gracefully
511
+ it "registers necessary callbacks" do
512
+ expect(described_class).to have_callback(:before_execution)
542
513
  end
543
514
 
544
515
  context "when processing succeeds" do
@@ -550,10 +521,33 @@ describe ProcessOrderTask do
550
521
  .and have_runtime(be_positive)
551
522
  end
552
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
553
533
  end
554
534
  ```
555
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
+
556
550
  ---
557
551
 
558
552
  - **Prev:** [Internationalization (i18n)](internationalization.md)
559
- - **Next:** [AI Prompts](ai_prompts.md)
553
+ - **Next:** [Deprecation](deprecation.md)