cmdx 0.5.0 → 1.0.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 (151) hide show
  1. checksums.yaml +4 -4
  2. data/.DS_Store +0 -0
  3. data/.cursor/rules/cursor-instructions.mdc +6 -0
  4. data/.rubocop.yml +19 -1
  5. data/.ruby-version +1 -1
  6. data/CHANGELOG.md +95 -28
  7. data/README.md +73 -25
  8. data/docs/ai_prompts.md +319 -0
  9. data/docs/basics/call.md +234 -14
  10. data/docs/basics/chain.md +280 -0
  11. data/docs/basics/context.md +241 -33
  12. data/docs/basics/setup.md +85 -12
  13. data/docs/callbacks.md +283 -0
  14. data/docs/configuration.md +155 -30
  15. data/docs/getting_started.md +145 -22
  16. data/docs/internationalization.md +148 -0
  17. data/docs/interruptions/exceptions.md +198 -11
  18. data/docs/interruptions/faults.md +196 -44
  19. data/docs/interruptions/halt.md +188 -35
  20. data/docs/logging.md +204 -53
  21. data/docs/middlewares.md +745 -0
  22. data/docs/outcomes/result.md +305 -10
  23. data/docs/outcomes/states.md +212 -31
  24. data/docs/outcomes/statuses.md +284 -30
  25. data/docs/parameters/coercions.md +411 -29
  26. data/docs/parameters/defaults.md +258 -25
  27. data/docs/parameters/definitions.md +247 -72
  28. data/docs/parameters/namespacing.md +259 -27
  29. data/docs/parameters/validations.md +173 -168
  30. data/docs/testing.md +560 -0
  31. data/docs/tips_and_tricks.md +103 -42
  32. data/docs/workflows.md +329 -0
  33. data/lib/cmdx/.DS_Store +0 -0
  34. data/lib/cmdx/callback.rb +69 -0
  35. data/lib/cmdx/callback_registry.rb +106 -0
  36. data/lib/cmdx/chain.rb +190 -0
  37. data/lib/cmdx/chain_inspector.rb +149 -0
  38. data/lib/cmdx/chain_serializer.rb +175 -0
  39. data/lib/cmdx/coercions/array.rb +37 -0
  40. data/lib/cmdx/coercions/big_decimal.rb +33 -0
  41. data/lib/cmdx/coercions/boolean.rb +41 -1
  42. data/lib/cmdx/coercions/complex.rb +31 -0
  43. data/lib/cmdx/coercions/date.rb +39 -0
  44. data/lib/cmdx/coercions/date_time.rb +39 -0
  45. data/lib/cmdx/coercions/float.rb +31 -0
  46. data/lib/cmdx/coercions/hash.rb +42 -0
  47. data/lib/cmdx/coercions/integer.rb +32 -0
  48. data/lib/cmdx/coercions/rational.rb +31 -0
  49. data/lib/cmdx/coercions/string.rb +31 -0
  50. data/lib/cmdx/coercions/time.rb +39 -0
  51. data/lib/cmdx/coercions/virtual.rb +31 -0
  52. data/lib/cmdx/configuration.rb +217 -9
  53. data/lib/cmdx/context.rb +173 -2
  54. data/lib/cmdx/core_ext/hash.rb +72 -0
  55. data/lib/cmdx/core_ext/module.rb +94 -0
  56. data/lib/cmdx/core_ext/object.rb +105 -0
  57. data/lib/cmdx/correlator.rb +217 -0
  58. data/lib/cmdx/error.rb +210 -8
  59. data/lib/cmdx/errors.rb +256 -1
  60. data/lib/cmdx/fault.rb +177 -2
  61. data/lib/cmdx/faults.rb +158 -2
  62. data/lib/cmdx/immutator.rb +121 -2
  63. data/lib/cmdx/lazy_struct.rb +261 -18
  64. data/lib/cmdx/log_formatters/json.rb +46 -0
  65. data/lib/cmdx/log_formatters/key_value.rb +46 -0
  66. data/lib/cmdx/log_formatters/line.rb +54 -0
  67. data/lib/cmdx/log_formatters/logstash.rb +64 -0
  68. data/lib/cmdx/log_formatters/pretty_json.rb +57 -0
  69. data/lib/cmdx/log_formatters/pretty_key_value.rb +51 -0
  70. data/lib/cmdx/log_formatters/pretty_line.rb +60 -0
  71. data/lib/cmdx/log_formatters/raw.rb +54 -0
  72. data/lib/cmdx/logger.rb +85 -0
  73. data/lib/cmdx/logger_ansi.rb +93 -7
  74. data/lib/cmdx/logger_serializer.rb +116 -0
  75. data/lib/cmdx/middleware.rb +74 -0
  76. data/lib/cmdx/middleware_registry.rb +106 -0
  77. data/lib/cmdx/middlewares/correlate.rb +266 -0
  78. data/lib/cmdx/middlewares/timeout.rb +232 -0
  79. data/lib/cmdx/parameter.rb +228 -1
  80. data/lib/cmdx/parameter_inspector.rb +61 -0
  81. data/lib/cmdx/parameter_registry.rb +125 -0
  82. data/lib/cmdx/parameter_serializer.rb +83 -0
  83. data/lib/cmdx/parameter_validator.rb +62 -0
  84. data/lib/cmdx/parameter_value.rb +109 -1
  85. data/lib/cmdx/parameters_inspector.rb +59 -0
  86. data/lib/cmdx/parameters_serializer.rb +102 -0
  87. data/lib/cmdx/railtie.rb +123 -3
  88. data/lib/cmdx/result.rb +367 -25
  89. data/lib/cmdx/result_ansi.rb +105 -9
  90. data/lib/cmdx/result_inspector.rb +76 -0
  91. data/lib/cmdx/result_logger.rb +90 -3
  92. data/lib/cmdx/result_serializer.rb +137 -0
  93. data/lib/cmdx/rspec/result_matchers.rb +917 -0
  94. data/lib/cmdx/rspec/task_matchers.rb +570 -0
  95. data/lib/cmdx/task.rb +405 -37
  96. data/lib/cmdx/task_serializer.rb +74 -2
  97. data/lib/cmdx/utils/ansi_color.rb +95 -0
  98. data/lib/cmdx/utils/log_timestamp.rb +48 -0
  99. data/lib/cmdx/utils/monotonic_runtime.rb +71 -4
  100. data/lib/cmdx/utils/name_affix.rb +78 -0
  101. data/lib/cmdx/validators/custom.rb +82 -0
  102. data/lib/cmdx/validators/exclusion.rb +94 -0
  103. data/lib/cmdx/validators/format.rb +102 -8
  104. data/lib/cmdx/validators/inclusion.rb +104 -0
  105. data/lib/cmdx/validators/length.rb +128 -0
  106. data/lib/cmdx/validators/numeric.rb +128 -0
  107. data/lib/cmdx/validators/presence.rb +93 -7
  108. data/lib/cmdx/version.rb +7 -1
  109. data/lib/cmdx/workflow.rb +394 -0
  110. data/lib/cmdx.rb +25 -64
  111. data/lib/generators/cmdx/install_generator.rb +37 -1
  112. data/lib/generators/cmdx/task_generator.rb +69 -1
  113. data/lib/generators/cmdx/templates/install.rb +43 -15
  114. data/lib/generators/cmdx/workflow_generator.rb +109 -0
  115. data/lib/locales/ar.yml +36 -0
  116. data/lib/locales/cs.yml +36 -0
  117. data/lib/locales/da.yml +36 -0
  118. data/lib/locales/de.yml +36 -0
  119. data/lib/locales/el.yml +36 -0
  120. data/lib/locales/en.yml +20 -20
  121. data/lib/locales/es.yml +20 -20
  122. data/lib/locales/fi.yml +36 -0
  123. data/lib/locales/fr.yml +36 -0
  124. data/lib/locales/he.yml +36 -0
  125. data/lib/locales/hi.yml +36 -0
  126. data/lib/locales/it.yml +36 -0
  127. data/lib/locales/ja.yml +36 -0
  128. data/lib/locales/ko.yml +36 -0
  129. data/lib/locales/nl.yml +36 -0
  130. data/lib/locales/no.yml +36 -0
  131. data/lib/locales/pl.yml +36 -0
  132. data/lib/locales/pt.yml +36 -0
  133. data/lib/locales/ru.yml +36 -0
  134. data/lib/locales/sv.yml +36 -0
  135. data/lib/locales/th.yml +36 -0
  136. data/lib/locales/tr.yml +36 -0
  137. data/lib/locales/vi.yml +36 -0
  138. data/lib/locales/zh.yml +36 -0
  139. metadata +77 -15
  140. data/docs/basics/run.md +0 -34
  141. data/docs/batch.md +0 -53
  142. data/docs/example.md +0 -82
  143. data/docs/hooks.md +0 -62
  144. data/lib/cmdx/batch.rb +0 -43
  145. data/lib/cmdx/parameters.rb +0 -35
  146. data/lib/cmdx/run.rb +0 -39
  147. data/lib/cmdx/run_inspector.rb +0 -26
  148. data/lib/cmdx/run_serializer.rb +0 -20
  149. data/lib/cmdx/task_hook.rb +0 -18
  150. data/lib/generators/cmdx/batch_generator.rb +0 -30
  151. /data/lib/generators/cmdx/templates/{batch.rb.tt → workflow.rb.tt} +0 -0
data/docs/testing.md ADDED
@@ -0,0 +1,560 @@
1
+ # Testing
2
+
3
+ CMDx provides a comprehensive suite of custom RSpec matchers designed for expressive, maintainable testing of tasks, results, and business logic workflows.
4
+
5
+ ## Table of Contents
6
+
7
+ - [TLDR](#tldr)
8
+ - [External Project Setup](#external-project-setup)
9
+ - [Matcher Organization](#matcher-organization)
10
+ - [Result Matchers](#result-matchers)
11
+ - [Primary Outcome Matchers](#primary-outcome-matchers)
12
+ - [State and Status Matchers](#state-and-status-matchers)
13
+ - [Metadata and Context Matchers](#metadata-and-context-matchers)
14
+ - [Failure Chain Matchers](#failure-chain-matchers)
15
+ - [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)
19
+ - [Callback and Middleware Matchers](#callback-and-middleware-matchers)
20
+ - [Configuration Matchers](#configuration-matchers)
21
+ - [Composable Testing](#composable-testing)
22
+ - [Best Practices](#best-practices)
23
+
24
+ ## TLDR
25
+
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
32
+
33
+ ## External Project Setup
34
+
35
+ To use CMDx's custom matchers in an external RSpec-based project update your `spec/spec_helper.rb` or `spec/rails_helper.rb`:
36
+
37
+ ```ruby
38
+ require "cmdx/rspec/result_matchers"
39
+ require "cmdx/rspec/task_matchers"
40
+ ```
41
+
42
+ ## Matcher Organization
43
+
44
+ CMDx matchers are organized into two primary files with comprehensive YARD documentation:
45
+
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 |
50
+
51
+ All matchers include:
52
+ - Complete parameter descriptions
53
+ - Multiple usage examples
54
+ - Return value specifications
55
+ - Negation examples
56
+ - Version information
57
+
58
+ ## Result Matchers
59
+
60
+ ### Primary Outcome Matchers
61
+
62
+ These composite matchers validate complete task execution scenarios with single assertions:
63
+
64
+ #### Successful Task Validation
65
+
66
+ ```ruby
67
+ # Basic successful task validation
68
+ expect(result).to be_successful_task
69
+
70
+ # Successful task with context validation
71
+ expect(result).to be_successful_task(user_id: 123, processed: true)
72
+
73
+ # Negated usage
74
+ expect(result).not_to be_successful_task
75
+ ```
76
+
77
+ **What it validates:**
78
+ - Result has success status
79
+ - Result is in complete state
80
+ - Result was executed
81
+ - Optional context attributes match
82
+
83
+ #### Failed Task Validation
84
+
85
+ ```ruby
86
+ # Basic failed task validation
87
+ expect(result).to be_failed_task
88
+
89
+ # Failed task with specific reason
90
+ expect(result).to be_failed_task("Validation failed")
91
+
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)
96
+
97
+ # Negated usage
98
+ expect(result).not_to be_failed_task
99
+ ```
100
+
101
+ **What it validates:**
102
+ - Result has failed status
103
+ - Result is in interrupted state
104
+ - Result was executed
105
+ - Optional reason and metadata match
106
+
107
+ #### Skipped Task Validation
108
+
109
+ ```ruby
110
+ # Basic skipped task validation
111
+ expect(result).to be_skipped_task
112
+
113
+ # Skipped task with specific reason
114
+ expect(result).to be_skipped_task("Already processed")
115
+
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")
120
+
121
+ # Negated usage
122
+ expect(result).not_to be_skipped_task
123
+ ```
124
+
125
+ **What it validates:**
126
+ - Result has skipped status
127
+ - Result is in interrupted state
128
+ - Result was executed
129
+ - Optional reason and metadata match
130
+
131
+ ### State and Status Matchers
132
+
133
+ Individual validation matchers for granular testing:
134
+
135
+ #### Execution State Matchers
136
+
137
+ ```ruby
138
+ # Individual state checks (auto-generated from CMDx::Result::STATES)
139
+ expect(result).to be_initialized
140
+ expect(result).to be_executing
141
+ expect(result).to be_complete
142
+ expect(result).to be_interrupted
143
+
144
+ # Negated usage
145
+ expect(result).not_to be_initialized
146
+ ```
147
+
148
+ #### Execution Status Matchers
149
+
150
+ ```ruby
151
+ # Individual status checks (auto-generated from CMDx::Result::STATUSES)
152
+ expect(result).to be_success
153
+ expect(result).to be_skipped
154
+ expect(result).to be_failed
155
+
156
+ # Negated usage
157
+ expect(result).not_to be_success
158
+ ```
159
+
160
+ #### Execution and Outcome Matchers
161
+
162
+ ```ruby
163
+ # Execution validation
164
+ expect(result).to be_executed
165
+
166
+ # Outcome classification
167
+ 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
173
+ ```
174
+
175
+ ### Metadata and Context Matchers
176
+
177
+ #### Metadata Validation
178
+
179
+ ```ruby
180
+ # Basic metadata validation with RSpec matcher support
181
+ expect(result).to have_metadata(reason: "Error", code: "001")
182
+ 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)
188
+ )
189
+
190
+ # Chainable metadata inclusion
191
+ expect(result).to have_metadata(reason: "Error")
192
+ .including(code: "001", retryable: false)
193
+
194
+ # Empty metadata validation
195
+ expect(result).to have_empty_metadata
196
+
197
+ # Negated usage
198
+ expect(result).not_to have_metadata(reason: "Different error")
199
+ ```
200
+
201
+ #### Runtime Validation
202
+
203
+ ```ruby
204
+ # Basic runtime presence validation
205
+ expect(result).to have_runtime
206
+
207
+ # Runtime with specific value
208
+ expect(result).to have_runtime(0.5)
209
+
210
+ # Runtime with RSpec matchers
211
+ expect(result).to have_runtime(be > 0)
212
+ expect(result).to have_runtime(be_within(0.1).of(0.5))
213
+
214
+ # Negated usage
215
+ expect(result).not_to have_runtime
216
+ ```
217
+
218
+ #### Context Side Effects
219
+
220
+ ```ruby
221
+ # Context validation with RSpec matcher support
222
+ 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
+
229
+ # Complex side effects validation
230
+ expect(result).to have_context(
231
+ user: have_attributes(id: 123, name: "John"),
232
+ notifications: contain_exactly("email", "sms")
233
+ )
234
+
235
+ # Context preservation
236
+ expect(result).to preserve_context(original_data)
237
+
238
+ # Negated usage
239
+ expect(result).not_to have_context(deleted: true)
240
+ ```
241
+
242
+ #### Chain Validation
243
+
244
+ ```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
+ # Chain position validation
252
+ expect(result).to have_chain_index(0) # First task in chain
253
+ expect(result).to have_chain_index(2) # Third task in chain
254
+
255
+ # Negated usage
256
+ expect(result).not_to belong_to_chain
257
+ expect(result).not_to have_chain_index(1)
258
+ ```
259
+
260
+ ### Failure Chain Matchers
261
+
262
+ Test CMDx's failure propagation patterns:
263
+
264
+ #### Original Failure Validation
265
+
266
+ ```ruby
267
+ # Test that result represents an original failure (not propagated)
268
+ expect(result).to have_caused_failure
269
+
270
+ # Negated usage (for thrown failures)
271
+ expect(result).not_to have_caused_failure
272
+ ```
273
+
274
+ #### Failure Propagation Validation
275
+
276
+ ```ruby
277
+ # Basic thrown failure validation
278
+ expect(result).to have_thrown_failure
279
+
280
+ # 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
285
+ ```
286
+
287
+ #### Received Failure Validation
288
+
289
+ ```ruby
290
+ # Test that result received a thrown failure
291
+ expect(result).to have_received_thrown_failure
292
+
293
+ # Negated usage
294
+ expect(result).not_to have_received_thrown_failure
295
+ ```
296
+
297
+ ## Task Matchers
298
+
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
345
+
346
+ #### Well-Formed Task Validation
347
+
348
+ ```ruby
349
+ # Test task meets all structural requirements
350
+ expect(MyTask).to be_well_formed_task
351
+ expect(UserCreationTask).to be_well_formed_task
352
+
353
+ # Negated usage (for malformed tasks)
354
+ expect(BrokenTask).not_to be_well_formed_task
355
+ ```
356
+
357
+ **What it validates:**
358
+ - Inherits from CMDx::Task
359
+ - Implements required call method
360
+ - Has properly initialized parameter, callback, and middleware registries
361
+
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.
375
+
376
+ #### Bang Method Exception Propagation
377
+
378
+ ```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
384
+ ```
385
+
386
+ **How it works:** Tests that `call!` method propagates exceptions instead of handling them gracefully.
387
+
388
+ ### Callback and Middleware Matchers
389
+
390
+ #### Callback Registration Testing
391
+
392
+ ```ruby
393
+ # Test basic callback registration
394
+ expect(ValidatedTask).to have_callback(:before_validation)
395
+ expect(NotifiedTask).to have_callback(:on_success)
396
+ expect(CleanupTask).to have_callback(:after_execution)
397
+
398
+ # Test callback with specific callable
399
+ 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
+ ```
404
+
405
+ #### Callback Execution Testing
406
+
407
+ ```ruby
408
+ # 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)
417
+ ```
418
+
419
+ > [!NOTE]
420
+ > Callback execution testing may require mocking internal callback mechanisms for comprehensive validation.
421
+
422
+ #### Middleware Registration Testing
423
+
424
+ ```ruby
425
+ # Test middleware registration
426
+ expect(AuthenticatedTask).to have_middleware(AuthenticationMiddleware)
427
+ expect(LoggedTask).to have_middleware(LoggingMiddleware)
428
+ expect(TimedTask).to have_middleware(TimeoutMiddleware)
429
+
430
+ # Negated usage
431
+ expect(SimpleTask).not_to have_middleware(ComplexMiddleware)
432
+ ```
433
+
434
+ ### Configuration Matchers
435
+
436
+ #### Task Setting Validation
437
+
438
+ ```ruby
439
+ # Test setting presence
440
+ expect(ConfiguredTask).to have_task_setting(:timeout)
441
+ expect(CustomTask).to have_task_setting(:priority)
442
+
443
+ # 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)
449
+ ```
450
+
451
+ ## Composable Testing
452
+
453
+ Following RSpec best practices, CMDx matchers are designed for composition:
454
+
455
+ ### Chaining with `.and`
456
+
457
+ ```ruby
458
+ # Chain multiple result expectations
459
+ expect(result).to be_successful_task(user_id: 123)
460
+ .and have_context(processed_at: be_a(Time))
461
+ .and have_runtime(be > 0)
462
+ .and belong_to_chain
463
+
464
+ # Chain task validation expectations
465
+ expect(TaskClass).to be_well_formed_task
466
+ .and validate_required_parameter(:user_id)
467
+ .and have_callback(:before_execution)
468
+ .and handle_exceptions_gracefully
469
+ ```
470
+
471
+ ### Integration with Built-in RSpec Matchers
472
+
473
+ ```ruby
474
+ # Combine with built-in matchers
475
+ expect(result).to be_failed_task
476
+ .with_metadata(error_code: "ERR001", retryable: be_falsy)
477
+ .and have_caused_failure
478
+
479
+ # Use in complex scenarios
480
+ expect(result).to be_successful_task
481
+ .and have_context(
482
+ user: have_attributes(id: be_a(Integer), email: match(/@/)),
483
+ timestamps: all(be_a(Time)),
484
+ notifications: contain_exactly("email", "sms")
485
+ )
486
+ ```
487
+
488
+ ## Best Practices
489
+
490
+ ### 1. Use Composite Matchers When Possible
491
+
492
+ **Preferred:**
493
+ ```ruby
494
+ expect(result).to be_successful_task(user_id: 123)
495
+ ```
496
+
497
+ **Instead of:**
498
+ ```ruby
499
+ expect(result).to be_a(CMDx::Result)
500
+ expect(result).to be_success
501
+ expect(result).to be_complete
502
+ expect(result).to be_executed
503
+ expect(result.context.user_id).to eq(123)
504
+ ```
505
+
506
+ ### 2. Combine Granular and Composite Testing
507
+
508
+ Use composite matchers for primary assertions, granular matchers for specific edge cases:
509
+
510
+ ```ruby
511
+ # Primary assertion
512
+ expect(result).to be_successful_task
513
+
514
+ # Specific edge case validation
515
+ expect(result).to have_runtime(be < 1.0) # Performance requirement
516
+ expect(result).to have_chain_index(0) # Position validation
517
+ ```
518
+
519
+ ### 3. Leverage RSpec Matcher Integration
520
+
521
+ CMDx matchers work seamlessly with built-in RSpec matchers:
522
+
523
+ ```ruby
524
+ expect(result).to have_metadata(
525
+ timestamp: be_within(1.second).of(Time.current),
526
+ errors: be_empty,
527
+ count: be_between(1, 100)
528
+ )
529
+ ```
530
+
531
+ ### 4. Write Descriptive Test Names
532
+
533
+ Matcher names are designed to read naturally in test descriptions:
534
+
535
+ ```ruby
536
+ describe ProcessOrderTask do
537
+ it "validates required parameters" do
538
+ expect(described_class).to validate_required_parameter(:order_id)
539
+ end
540
+
541
+ it "handles exceptions gracefully" do
542
+ expect(described_class).to handle_exceptions_gracefully
543
+ end
544
+
545
+ context "when processing succeeds" do
546
+ it "returns successful result with order data" do
547
+ result = described_class.call(order_id: 123)
548
+
549
+ expect(result).to be_successful_task(order_id: 123)
550
+ .and have_context(order: be_present, processed_at: be_a(Time))
551
+ .and have_runtime(be_positive)
552
+ end
553
+ end
554
+ end
555
+ ```
556
+
557
+ ---
558
+
559
+ - **Prev:** [Internationalization (i18n)](internationalization.md)
560
+ - **Next:** [AI Prompts](ai_prompts.md)