cmdx 1.0.0 → 1.1.0

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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/.cursor/prompts/rspec.md +20 -0
  3. data/.cursor/prompts/yardoc.md +8 -0
  4. data/.rubocop.yml +5 -0
  5. data/CHANGELOG.md +101 -49
  6. data/README.md +2 -1
  7. data/docs/ai_prompts.md +10 -0
  8. data/docs/basics/call.md +11 -2
  9. data/docs/basics/chain.md +10 -1
  10. data/docs/basics/context.md +9 -0
  11. data/docs/basics/setup.md +9 -0
  12. data/docs/callbacks.md +14 -37
  13. data/docs/configuration.md +68 -27
  14. data/docs/getting_started.md +11 -0
  15. data/docs/internationalization.md +148 -0
  16. data/docs/interruptions/exceptions.md +10 -1
  17. data/docs/interruptions/faults.md +11 -2
  18. data/docs/interruptions/halt.md +9 -0
  19. data/docs/logging.md +14 -4
  20. data/docs/middlewares.md +53 -43
  21. data/docs/outcomes/result.md +9 -0
  22. data/docs/outcomes/states.md +9 -0
  23. data/docs/outcomes/statuses.md +9 -0
  24. data/docs/parameters/coercions.md +58 -38
  25. data/docs/parameters/defaults.md +10 -1
  26. data/docs/parameters/definitions.md +9 -0
  27. data/docs/parameters/namespacing.md +9 -0
  28. data/docs/parameters/validations.md +8 -67
  29. data/docs/testing.md +22 -13
  30. data/docs/tips_and_tricks.md +9 -0
  31. data/docs/workflows.md +14 -4
  32. data/lib/cmdx/.DS_Store +0 -0
  33. data/lib/cmdx/callback.rb +36 -56
  34. data/lib/cmdx/callback_registry.rb +82 -73
  35. data/lib/cmdx/chain.rb +65 -122
  36. data/lib/cmdx/chain_inspector.rb +22 -115
  37. data/lib/cmdx/chain_serializer.rb +17 -148
  38. data/lib/cmdx/coercion.rb +49 -0
  39. data/lib/cmdx/coercion_registry.rb +94 -0
  40. data/lib/cmdx/coercions/array.rb +18 -36
  41. data/lib/cmdx/coercions/big_decimal.rb +21 -33
  42. data/lib/cmdx/coercions/boolean.rb +21 -40
  43. data/lib/cmdx/coercions/complex.rb +18 -31
  44. data/lib/cmdx/coercions/date.rb +20 -39
  45. data/lib/cmdx/coercions/date_time.rb +22 -39
  46. data/lib/cmdx/coercions/float.rb +19 -32
  47. data/lib/cmdx/coercions/hash.rb +22 -41
  48. data/lib/cmdx/coercions/integer.rb +20 -33
  49. data/lib/cmdx/coercions/rational.rb +20 -32
  50. data/lib/cmdx/coercions/string.rb +23 -31
  51. data/lib/cmdx/coercions/time.rb +24 -40
  52. data/lib/cmdx/coercions/virtual.rb +14 -31
  53. data/lib/cmdx/configuration.rb +57 -171
  54. data/lib/cmdx/context.rb +22 -165
  55. data/lib/cmdx/core_ext/hash.rb +42 -67
  56. data/lib/cmdx/core_ext/module.rb +35 -79
  57. data/lib/cmdx/core_ext/object.rb +63 -98
  58. data/lib/cmdx/correlator.rb +40 -156
  59. data/lib/cmdx/error.rb +37 -202
  60. data/lib/cmdx/errors.rb +165 -202
  61. data/lib/cmdx/fault.rb +55 -158
  62. data/lib/cmdx/faults.rb +26 -137
  63. data/lib/cmdx/immutator.rb +22 -109
  64. data/lib/cmdx/lazy_struct.rb +103 -187
  65. data/lib/cmdx/log_formatters/json.rb +14 -40
  66. data/lib/cmdx/log_formatters/key_value.rb +14 -40
  67. data/lib/cmdx/log_formatters/line.rb +14 -48
  68. data/lib/cmdx/log_formatters/logstash.rb +14 -57
  69. data/lib/cmdx/log_formatters/pretty_json.rb +14 -50
  70. data/lib/cmdx/log_formatters/pretty_key_value.rb +13 -46
  71. data/lib/cmdx/log_formatters/pretty_line.rb +16 -54
  72. data/lib/cmdx/log_formatters/raw.rb +19 -49
  73. data/lib/cmdx/logger.rb +20 -82
  74. data/lib/cmdx/logger_ansi.rb +18 -75
  75. data/lib/cmdx/logger_serializer.rb +24 -114
  76. data/lib/cmdx/middleware.rb +38 -60
  77. data/lib/cmdx/middleware_registry.rb +81 -77
  78. data/lib/cmdx/middlewares/correlate.rb +41 -226
  79. data/lib/cmdx/middlewares/timeout.rb +46 -185
  80. data/lib/cmdx/parameter.rb +120 -198
  81. data/lib/cmdx/parameter_evaluator.rb +231 -0
  82. data/lib/cmdx/parameter_inspector.rb +25 -56
  83. data/lib/cmdx/parameter_registry.rb +59 -84
  84. data/lib/cmdx/parameter_serializer.rb +23 -74
  85. data/lib/cmdx/railtie.rb +24 -107
  86. data/lib/cmdx/result.rb +254 -260
  87. data/lib/cmdx/result_ansi.rb +19 -85
  88. data/lib/cmdx/result_inspector.rb +27 -68
  89. data/lib/cmdx/result_logger.rb +18 -81
  90. data/lib/cmdx/result_serializer.rb +28 -132
  91. data/lib/cmdx/rspec/matchers.rb +28 -0
  92. data/lib/cmdx/rspec/result_matchers/be_executed.rb +42 -0
  93. data/lib/cmdx/rspec/result_matchers/be_failed_task.rb +94 -0
  94. data/lib/cmdx/rspec/result_matchers/be_skipped_task.rb +94 -0
  95. data/lib/cmdx/rspec/result_matchers/be_state_matchers.rb +59 -0
  96. data/lib/cmdx/rspec/result_matchers/be_status_matchers.rb +57 -0
  97. data/lib/cmdx/rspec/result_matchers/be_successful_task.rb +87 -0
  98. data/lib/cmdx/rspec/result_matchers/have_bad_outcome.rb +51 -0
  99. data/lib/cmdx/rspec/result_matchers/have_caused_failure.rb +58 -0
  100. data/lib/cmdx/rspec/result_matchers/have_chain_index.rb +59 -0
  101. data/lib/cmdx/rspec/result_matchers/have_context.rb +86 -0
  102. data/lib/cmdx/rspec/result_matchers/have_empty_metadata.rb +54 -0
  103. data/lib/cmdx/rspec/result_matchers/have_good_outcome.rb +52 -0
  104. data/lib/cmdx/rspec/result_matchers/have_metadata.rb +114 -0
  105. data/lib/cmdx/rspec/result_matchers/have_preserved_context.rb +66 -0
  106. data/lib/cmdx/rspec/result_matchers/have_received_thrown_failure.rb +64 -0
  107. data/lib/cmdx/rspec/result_matchers/have_runtime.rb +78 -0
  108. data/lib/cmdx/rspec/result_matchers/have_thrown_failure.rb +76 -0
  109. data/lib/cmdx/rspec/task_matchers/be_well_formed_task.rb +62 -0
  110. data/lib/cmdx/rspec/task_matchers/have_callback.rb +85 -0
  111. data/lib/cmdx/rspec/task_matchers/have_cmd_setting.rb +68 -0
  112. data/lib/cmdx/rspec/task_matchers/have_executed_callbacks.rb +92 -0
  113. data/lib/cmdx/rspec/task_matchers/have_middleware.rb +46 -0
  114. data/lib/cmdx/rspec/task_matchers/have_parameter.rb +181 -0
  115. data/lib/cmdx/task.rb +213 -425
  116. data/lib/cmdx/task_deprecator.rb +55 -0
  117. data/lib/cmdx/task_processor.rb +245 -0
  118. data/lib/cmdx/task_serializer.rb +22 -70
  119. data/lib/cmdx/utils/ansi_color.rb +13 -89
  120. data/lib/cmdx/utils/log_timestamp.rb +13 -42
  121. data/lib/cmdx/utils/monotonic_runtime.rb +13 -63
  122. data/lib/cmdx/utils/name_affix.rb +21 -71
  123. data/lib/cmdx/validator.rb +48 -0
  124. data/lib/cmdx/validator_registry.rb +86 -0
  125. data/lib/cmdx/validators/exclusion.rb +55 -94
  126. data/lib/cmdx/validators/format.rb +31 -85
  127. data/lib/cmdx/validators/inclusion.rb +65 -110
  128. data/lib/cmdx/validators/length.rb +117 -133
  129. data/lib/cmdx/validators/numeric.rb +123 -130
  130. data/lib/cmdx/validators/presence.rb +38 -79
  131. data/lib/cmdx/version.rb +1 -7
  132. data/lib/cmdx/workflow.rb +46 -339
  133. data/lib/cmdx.rb +1 -1
  134. data/lib/generators/cmdx/install_generator.rb +14 -31
  135. data/lib/generators/cmdx/task_generator.rb +39 -55
  136. data/lib/generators/cmdx/templates/install.rb +61 -11
  137. data/lib/generators/cmdx/workflow_generator.rb +41 -66
  138. data/lib/locales/ar.yml +35 -0
  139. data/lib/locales/cs.yml +35 -0
  140. data/lib/locales/da.yml +35 -0
  141. data/lib/locales/de.yml +35 -0
  142. data/lib/locales/el.yml +35 -0
  143. data/lib/locales/en.yml +19 -20
  144. data/lib/locales/es.yml +19 -20
  145. data/lib/locales/fi.yml +35 -0
  146. data/lib/locales/fr.yml +35 -0
  147. data/lib/locales/he.yml +35 -0
  148. data/lib/locales/hi.yml +35 -0
  149. data/lib/locales/it.yml +35 -0
  150. data/lib/locales/ja.yml +35 -0
  151. data/lib/locales/ko.yml +35 -0
  152. data/lib/locales/nl.yml +35 -0
  153. data/lib/locales/no.yml +35 -0
  154. data/lib/locales/pl.yml +35 -0
  155. data/lib/locales/pt.yml +35 -0
  156. data/lib/locales/ru.yml +35 -0
  157. data/lib/locales/sv.yml +35 -0
  158. data/lib/locales/th.yml +35 -0
  159. data/lib/locales/tr.yml +35 -0
  160. data/lib/locales/vi.yml +35 -0
  161. data/lib/locales/zh.yml +35 -0
  162. metadata +57 -8
  163. data/lib/cmdx/parameter_validator.rb +0 -81
  164. data/lib/cmdx/parameter_value.rb +0 -244
  165. data/lib/cmdx/parameters_inspector.rb +0 -72
  166. data/lib/cmdx/parameters_serializer.rb +0 -115
  167. data/lib/cmdx/rspec/result_matchers.rb +0 -917
  168. data/lib/cmdx/rspec/task_matchers.rb +0 -570
  169. data/lib/cmdx/validators/custom.rb +0 -102
@@ -1,917 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Custom RSpec matchers for CMDx result testing
4
- #
5
- # This module provides a comprehensive set of custom RSpec matchers specifically
6
- # designed for testing CMDx task execution results. These matchers follow the
7
- # RSpec Style Guide conventions and provide expressive, readable test assertions
8
- # for task outcomes, side effects, and execution state.
9
- #
10
- # The matchers are automatically loaded when the spec_helper is required and are
11
- # available in all RSpec test contexts.
12
- #
13
- # @example Basic result outcome testing
14
- # expect(result).to be_successful_task
15
- # expect(result).to be_failed_task.with_reason("Validation failed")
16
- # expect(result).to be_skipped_task("Already processed")
17
- #
18
- # @example Side effects and context testing
19
- # expect(result).to have_context(user_id: 123, processed: true)
20
- # expect(result).to preserve_context(original_data)
21
- #
22
- # @example Composable matcher usage
23
- # expect(result).to be_successful_task(user_id: 123)
24
- # .and have_context(processed_at: be_a(Time))
25
- # .and have_runtime(be > 0)
26
- #
27
- # @see https://rspec.rubystyle.guide/ RSpec Style Guide
28
- # @since 1.0.0
29
-
30
- # Tests that a task result represents a successful execution
31
- #
32
- # This matcher verifies that a result has a success status, complete state,
33
- # and was executed. Optionally validates specific context attributes.
34
- #
35
- # @param [Hash] expected_context Optional hash of context attributes to validate
36
- #
37
- # @example Basic successful task validation
38
- # expect(result).to be_successful_task
39
- #
40
- # @example Successful task with context validation
41
- # expect(result).to be_successful_task(user_id: 123, processed: true)
42
- #
43
- # @example Negated usage
44
- # expect(result).not_to be_successful_task
45
- #
46
- # @return [Boolean] true if result is successful, complete, and executed
47
- #
48
- # @since 1.0.0
49
- RSpec::Matchers.define :be_successful_task do |expected_context = {}|
50
- match do |result|
51
- result.success? &&
52
- result.complete? &&
53
- result.executed? &&
54
- (expected_context.empty? || context_matches?(result, expected_context))
55
- end
56
-
57
- failure_message do |result|
58
- messages = []
59
- messages << "expected result to be successful, but was #{result.status}" unless result.success?
60
- messages << "expected result to be complete, but was #{result.state}" unless result.complete?
61
- messages << "expected result to be executed, but was not" unless result.executed?
62
-
63
- unless expected_context.empty?
64
- mismatches = context_mismatches(result, expected_context)
65
- messages << "expected context to match #{expected_context}, but #{mismatches}" if mismatches.any?
66
- end
67
-
68
- messages.join(", ")
69
- end
70
-
71
- failure_message_when_negated do |_result|
72
- "expected result not to be successful, but it was"
73
- end
74
-
75
- description do
76
- desc = "be a successful task"
77
- desc += " with context #{expected_context}" unless expected_context.empty?
78
- desc
79
- end
80
-
81
- private
82
-
83
- def context_matches?(result, expected_context)
84
- expected_context.all? do |key, value|
85
- result.context.public_send(key) == value
86
- end
87
- end
88
-
89
- def context_mismatches(result, expected_context)
90
- expected_context.filter_map do |key, expected_value|
91
- actual_value = result.context.public_send(key)
92
- "#{key}: expected #{expected_value}, got #{actual_value}" if actual_value != expected_value
93
- end
94
- end
95
- end
96
-
97
- # Tests that a task result represents a failed execution
98
- #
99
- # This matcher verifies that a result has a failed status, interrupted state,
100
- # and was executed. Supports optional reason validation and chainable metadata checks.
101
- #
102
- # @param [String, nil] expected_reason Optional failure reason to validate
103
- #
104
- # @example Basic failed task validation
105
- # expect(result).to be_failed_task
106
- #
107
- # @example Failed task with specific reason
108
- # expect(result).to be_failed_task("Validation failed")
109
- #
110
- # @example Chainable reason and metadata validation
111
- # expect(result).to be_failed_task
112
- # .with_reason("Invalid data")
113
- # .with_metadata(error_code: "ERR001", retryable: false)
114
- #
115
- # @example Negated usage
116
- # expect(result).not_to be_failed_task
117
- #
118
- # @return [Boolean] true if result is failed, interrupted, and executed
119
- #
120
- # @since 1.0.0
121
- RSpec::Matchers.define :be_failed_task do |expected_reason = nil|
122
- match do |result|
123
- result.failed? &&
124
- result.interrupted? &&
125
- result.executed? &&
126
- (expected_reason.nil? || result.metadata[:reason] == expected_reason)
127
- end
128
-
129
- chain :with_reason do |reason|
130
- @expected_reason = reason
131
- end
132
-
133
- chain :with_metadata do |metadata|
134
- @expected_metadata = metadata
135
- end
136
-
137
- match do |result|
138
- reason = @expected_reason || expected_reason
139
- metadata = @expected_metadata || {}
140
-
141
- result.failed? &&
142
- result.interrupted? &&
143
- result.executed? &&
144
- (reason.nil? || result.metadata[:reason] == reason) &&
145
- (metadata.empty? || metadata.all? { |k, v| result.metadata[k] == v })
146
- end
147
-
148
- failure_message do |result|
149
- messages = []
150
- messages << "expected result to be failed, but was #{result.status}" unless result.failed?
151
- messages << "expected result to be interrupted, but was #{result.state}" unless result.interrupted?
152
- messages << "expected result to be executed, but was not" unless result.executed?
153
-
154
- reason = @expected_reason || expected_reason
155
- messages << "expected failure reason to be '#{reason}', but was '#{result.metadata[:reason]}'" if reason && result.metadata[:reason] != reason
156
-
157
- if @expected_metadata&.any?
158
- mismatches = @expected_metadata.filter_map do |k, v|
159
- "#{k}: expected #{v}, got #{result.metadata[k]}" if result.metadata[k] != v
160
- end
161
- messages.concat(mismatches)
162
- end
163
-
164
- messages.join(", ")
165
- end
166
-
167
- failure_message_when_negated do |_result|
168
- "expected result not to be failed, but it was"
169
- end
170
-
171
- description do
172
- desc = "be a failed task"
173
- reason = @expected_reason || expected_reason
174
- desc += " with reason '#{reason}'" if reason
175
- desc += " with metadata #{@expected_metadata}" if @expected_metadata&.any?
176
- desc
177
- end
178
- end
179
-
180
- # Tests that a task result represents a skipped execution
181
- #
182
- # This matcher verifies that a result has a skipped status, interrupted state,
183
- # and was executed. Supports optional reason validation and chainable metadata checks.
184
- #
185
- # @param [String, nil] expected_reason Optional skip reason to validate
186
- #
187
- # @example Basic skipped task validation
188
- # expect(result).to be_skipped_task
189
- #
190
- # @example Skipped task with specific reason
191
- # expect(result).to be_skipped_task("Already processed")
192
- #
193
- # @example Chainable reason and metadata validation
194
- # expect(result).to be_skipped_task
195
- # .with_reason("Order already processed")
196
- # .with_metadata(processed_at: be_a(Time), skip_code: "DUPLICATE")
197
- #
198
- # @example Negated usage
199
- # expect(result).not_to be_skipped_task
200
- #
201
- # @return [Boolean] true if result is skipped, interrupted, and executed
202
- #
203
- # @since 1.0.0
204
- RSpec::Matchers.define :be_skipped_task do |expected_reason = nil|
205
- match do |result|
206
- result.skipped? &&
207
- result.interrupted? &&
208
- result.executed? &&
209
- (expected_reason.nil? || result.metadata[:reason] == expected_reason)
210
- end
211
-
212
- chain :with_reason do |reason|
213
- @expected_reason = reason
214
- end
215
-
216
- chain :with_metadata do |metadata|
217
- @expected_metadata = metadata
218
- end
219
-
220
- match do |result|
221
- reason = @expected_reason || expected_reason
222
- metadata = @expected_metadata || {}
223
-
224
- result.skipped? &&
225
- result.interrupted? &&
226
- result.executed? &&
227
- (reason.nil? || result.metadata[:reason] == reason) &&
228
- (metadata.empty? || metadata.all? { |k, v| result.metadata[k] == v })
229
- end
230
-
231
- failure_message do |result|
232
- messages = []
233
- messages << "expected result to be skipped, but was #{result.status}" unless result.skipped?
234
- messages << "expected result to be interrupted, but was #{result.state}" unless result.interrupted?
235
- messages << "expected result to be executed, but was not" unless result.executed?
236
-
237
- reason = @expected_reason || expected_reason
238
- messages << "expected skip reason to be '#{reason}', but was '#{result.metadata[:reason]}'" if reason && result.metadata[:reason] != reason
239
-
240
- if @expected_metadata&.any?
241
- mismatches = @expected_metadata.filter_map do |k, v|
242
- "#{k}: expected #{v}, got #{result.metadata[k]}" if result.metadata[k] != v
243
- end
244
- messages.concat(mismatches)
245
- end
246
-
247
- messages.join(", ")
248
- end
249
-
250
- failure_message_when_negated do |_result|
251
- "expected result not to be skipped, but it was"
252
- end
253
-
254
- description do
255
- desc = "be a skipped task"
256
- reason = @expected_reason || expected_reason
257
- desc += " with reason '#{reason}'" if reason
258
- desc += " with metadata #{@expected_metadata}" if @expected_metadata&.any?
259
- desc
260
- end
261
- end
262
-
263
- # Tests that a task result has a good outcome (success or skipped)
264
- #
265
- # This matcher verifies that a result has either a success or skipped status,
266
- # representing a positive outcome where the task completed its intended purpose
267
- # or was appropriately bypassed.
268
- #
269
- # @example Basic good outcome validation
270
- # expect(result).to have_good_outcome
271
- #
272
- # @example Negated usage
273
- # expect(result).not_to have_good_outcome
274
- #
275
- # @return [Boolean] true if result is success or skipped
276
- #
277
- # @since 1.0.0
278
- RSpec::Matchers.define :have_good_outcome do
279
- match(&:good?)
280
-
281
- failure_message do |result|
282
- "expected result to have good outcome (success or skipped), but was #{result.status}"
283
- end
284
-
285
- failure_message_when_negated do |result|
286
- "expected result not to have good outcome, but it did (status: #{result.status})"
287
- end
288
-
289
- description do
290
- "have good outcome"
291
- end
292
- end
293
-
294
- # Tests that a task result has a bad outcome (not success)
295
- #
296
- # This matcher verifies that a result does not have a success status,
297
- # representing a negative outcome where the task did not complete successfully.
298
- #
299
- # @example Basic bad outcome validation
300
- # expect(result).to have_bad_outcome
301
- #
302
- # @example Negated usage
303
- # expect(result).not_to have_bad_outcome
304
- #
305
- # @return [Boolean] true if result is not success
306
- #
307
- # @since 1.0.0
308
- RSpec::Matchers.define :have_bad_outcome do
309
- match(&:bad?)
310
-
311
- failure_message do |result|
312
- "expected result to have bad outcome (not success), but was #{result.status}"
313
- end
314
-
315
- failure_message_when_negated do |result|
316
- "expected result not to have bad outcome, but it did (status: #{result.status})"
317
- end
318
-
319
- description do
320
- "have bad outcome"
321
- end
322
- end
323
-
324
- # Tests that a task result indicates the task was executed
325
- #
326
- # This matcher verifies that a result shows the task has moved beyond
327
- # the initialized state and has been processed by the execution engine.
328
- #
329
- # @example Basic execution validation
330
- # expect(result).to be_executed
331
- #
332
- # @example Negated usage
333
- # expect(result).not_to be_executed
334
- #
335
- # @return [Boolean] true if result indicates execution occurred
336
- #
337
- # @since 1.0.0
338
- RSpec::Matchers.define :be_executed do
339
- match(&:executed?)
340
-
341
- failure_message do |result|
342
- "expected result to be executed, but was in #{result.state} state"
343
- end
344
-
345
- failure_message_when_negated do |result|
346
- "expected result not to be executed, but it was (state: #{result.state})"
347
- end
348
-
349
- description do
350
- "be executed"
351
- end
352
- end
353
-
354
- # Tests that a task result has runtime information
355
- #
356
- # This matcher verifies that a result contains execution timing data.
357
- # Optionally validates the runtime against a specific value or matcher.
358
- #
359
- # @param [Numeric, RSpec::Matchers::BuiltIn::BaseMatcher, nil] expected_runtime
360
- # Optional runtime value or matcher to validate against
361
- #
362
- # @example Basic runtime presence validation
363
- # expect(result).to have_runtime
364
- #
365
- # @example Runtime with specific value
366
- # expect(result).to have_runtime(0.5)
367
- #
368
- # @example Runtime with matcher
369
- # expect(result).to have_runtime(be > 0)
370
- # expect(result).to have_runtime(be_within(0.1).of(0.5))
371
- #
372
- # @example Negated usage
373
- # expect(result).not_to have_runtime
374
- #
375
- # @return [Boolean] true if result has runtime (and matches expectation if provided)
376
- #
377
- # @since 1.0.0
378
- RSpec::Matchers.define :have_runtime do |expected_runtime = nil|
379
- match do |result|
380
- return false if result.runtime.nil?
381
- return true if expected_runtime.nil?
382
-
383
- if expected_runtime.respond_to?(:matches?)
384
- expected_runtime.matches?(result.runtime)
385
- else
386
- result.runtime == expected_runtime
387
- end
388
- end
389
-
390
- failure_message do |result|
391
- if result.runtime.nil?
392
- "expected result to have runtime, but it was nil"
393
- elsif expected_runtime
394
- "expected result runtime to #{expected_runtime}, but was #{result.runtime}"
395
- end
396
- end
397
-
398
- failure_message_when_negated do |result|
399
- if expected_runtime
400
- "expected result runtime not to #{expected_runtime}, but it was #{result.runtime}"
401
- else
402
- "expected result not to have runtime, but it was #{result.runtime}"
403
- end
404
- end
405
-
406
- description do
407
- if expected_runtime
408
- "have runtime #{expected_runtime}"
409
- else
410
- "have runtime"
411
- end
412
- end
413
- end
414
-
415
- # Tests that a task result contains specific metadata
416
- #
417
- # This matcher verifies that a result's metadata hash contains the expected
418
- # key-value pairs. Supports chainable inclusion for complex metadata validation.
419
- #
420
- # @param [Hash] expected_metadata Hash of metadata keys and values to validate
421
- #
422
- # @example Basic metadata validation
423
- # expect(result).to have_metadata(reason: "Error", code: "001")
424
- #
425
- # @example Chainable metadata inclusion
426
- # expect(result).to have_metadata(reason: "Error")
427
- # .including(code: "001", retryable: false)
428
- #
429
- # @example Empty metadata validation
430
- # expect(result).to have_metadata({})
431
- #
432
- # @example Negated usage
433
- # expect(result).not_to have_metadata(reason: "Different error")
434
- #
435
- # @return [Boolean] true if result metadata contains all expected key-value pairs
436
- #
437
- # @since 1.0.0
438
- RSpec::Matchers.define :have_metadata do |expected_metadata = {}|
439
- match do |result|
440
- expected_metadata.all? do |key, value|
441
- actual_value = result.metadata[key]
442
- if value.respond_to?(:matches?)
443
- value.matches?(actual_value)
444
- else
445
- actual_value == value
446
- end
447
- end
448
- end
449
-
450
- chain :including do |metadata|
451
- @additional_metadata = metadata
452
- end
453
-
454
- match do |result|
455
- all_metadata = expected_metadata.merge(@additional_metadata || {})
456
- all_metadata.all? do |key, value|
457
- actual_value = result.metadata[key]
458
- if value.respond_to?(:matches?)
459
- value.matches?(actual_value)
460
- else
461
- actual_value == value
462
- end
463
- end
464
- end
465
-
466
- failure_message do |result|
467
- all_metadata = expected_metadata.merge(@additional_metadata || {})
468
- mismatches = all_metadata.filter_map do |key, expected_value|
469
- actual_value = result.metadata[key]
470
- match_result = if expected_value.respond_to?(:matches?)
471
- expected_value.matches?(actual_value)
472
- else
473
- actual_value == expected_value
474
- end
475
- "#{key}: expected #{expected_value}, got #{actual_value}" unless match_result
476
- end
477
- "expected metadata to include #{all_metadata}, but #{mismatches.join(', ')}"
478
- end
479
-
480
- failure_message_when_negated do |_result|
481
- all_metadata = expected_metadata.merge(@additional_metadata || {})
482
- "expected metadata not to include #{all_metadata}, but it did"
483
- end
484
-
485
- description do
486
- all_metadata = expected_metadata.merge(@additional_metadata || {})
487
- "have metadata #{all_metadata}"
488
- end
489
- end
490
-
491
- # Tests that a task result has no metadata
492
- #
493
- # This matcher verifies that a result's metadata hash is empty,
494
- # indicating no additional execution information was recorded.
495
- #
496
- # @example Basic empty metadata validation
497
- # expect(result).to have_empty_metadata
498
- #
499
- # @example Negated usage
500
- # expect(result).not_to have_empty_metadata
501
- #
502
- # @return [Boolean] true if result metadata is empty
503
- #
504
- # @since 1.0.0
505
- RSpec::Matchers.define :have_empty_metadata do
506
- match do |result|
507
- result.metadata.empty?
508
- end
509
-
510
- failure_message do |result|
511
- "expected metadata to be empty, but was #{result.metadata}"
512
- end
513
-
514
- failure_message_when_negated do |_result|
515
- "expected metadata not to be empty, but it was"
516
- end
517
-
518
- description do
519
- "have empty metadata"
520
- end
521
- end
522
-
523
- # Tests that a task result has specific side effects in the context
524
- #
525
- # This matcher verifies that the result's context contains expected
526
- # attribute changes or additions that represent the task's side effects.
527
- # Supports both exact value matching and RSpec matcher integration.
528
- #
529
- # @param [Hash] expected_effects Hash of context attributes and expected values
530
- #
531
- # @example Basic side effects validation
532
- # expect(result).to have_context(processed: true, user_id: 123)
533
- #
534
- # @example Side effects with RSpec matchers
535
- # expect(result).to have_context(
536
- # processed_at: be_a(Time),
537
- # errors: be_empty,
538
- # count: be > 0
539
- # )
540
- #
541
- # @example Complex side effects validation
542
- # expect(result).to have_context(
543
- # user: have_attributes(id: 123, name: "John"),
544
- # notifications: contain_exactly("email", "sms")
545
- # )
546
- #
547
- # @example Negated usage
548
- # expect(result).not_to have_context(deleted: true)
549
- #
550
- # @return [Boolean] true if context contains all expected side effects
551
- #
552
- # @since 1.0.0
553
- RSpec::Matchers.define :have_context do |expected_effects|
554
- match do |result|
555
- expected_effects.all? do |key, expected_value|
556
- actual_value = result.context.public_send(key)
557
- if expected_value.respond_to?(:matches?)
558
- expected_value.matches?(actual_value)
559
- else
560
- actual_value == expected_value
561
- end
562
- end
563
- end
564
-
565
- failure_message do |result|
566
- mismatches = expected_effects.filter_map do |key, expected_value|
567
- actual_value = result.context.public_send(key)
568
- match_result = if expected_value.respond_to?(:matches?)
569
- expected_value.matches?(actual_value)
570
- else
571
- actual_value == expected_value
572
- end
573
-
574
- "#{key}: expected #{expected_value}, got #{actual_value}" unless match_result
575
- end
576
- "expected context to have side effects #{expected_effects}, but #{mismatches.join(', ')}"
577
- end
578
-
579
- failure_message_when_negated do |_result|
580
- "expected context not to have side effects #{expected_effects}, but it did"
581
- end
582
-
583
- description do
584
- "have side effects #{expected_effects}"
585
- end
586
- end
587
-
588
- # Tests that a task result preserves specific context attributes
589
- #
590
- # This matcher verifies that certain context attributes retain their
591
- # original values after task execution, ensuring data integrity for
592
- # attributes that should not be modified.
593
- #
594
- # @param [Hash] preserved_attributes Hash of attributes and their expected preserved values
595
- #
596
- # @example Basic context preservation validation
597
- # expect(result).to preserve_context(user_id: 123, session_id: "abc")
598
- #
599
- # @example Preserving complex data structures
600
- # expect(result).to preserve_context(
601
- # original_request: original_data,
602
- # user_permissions: ["read", "write"]
603
- # )
604
- #
605
- # @example Negated usage
606
- # expect(result).not_to preserve_context(temporary_flag: true)
607
- #
608
- # @return [Boolean] true if context preserves all specified attributes
609
- #
610
- # @since 1.0.0
611
- RSpec::Matchers.define :preserve_context do |preserved_attributes|
612
- match do |result|
613
- preserved_attributes.all? do |key, expected_value|
614
- result.context.public_send(key) == expected_value
615
- end
616
- end
617
-
618
- failure_message do |result|
619
- mismatches = preserved_attributes.filter_map do |key, expected_value|
620
- actual_value = result.context.public_send(key)
621
- "#{key}: expected #{expected_value}, got #{actual_value}" if actual_value != expected_value
622
- end
623
- "expected context to preserve #{preserved_attributes}, but #{mismatches.join(', ')}"
624
- end
625
-
626
- failure_message_when_negated do |_result|
627
- "expected context not to preserve #{preserved_attributes}, but it did"
628
- end
629
-
630
- description do
631
- "preserve context #{preserved_attributes}"
632
- end
633
- end
634
-
635
- # Tests that a task result represents a failure that was caused (not thrown)
636
- #
637
- # This matcher verifies that a failed result originated from the current task
638
- # rather than being propagated from another task. Used to distinguish between
639
- # original failures and failure propagation in task chains.
640
- #
641
- # @example Basic caused failure validation
642
- # expect(result).to have_caused_failure
643
- #
644
- # @example Negated usage (for thrown failures)
645
- # expect(result).not_to have_caused_failure
646
- #
647
- # @return [Boolean] true if result failed and caused the failure
648
- #
649
- # @since 1.0.0
650
- RSpec::Matchers.define :have_caused_failure do
651
- match do |result|
652
- result.failed? && result.caused_failure?
653
- end
654
-
655
- failure_message do |result|
656
- if result.failed?
657
- "expected result to have caused failure, but it threw/received a failure instead"
658
- else
659
- "expected result to have caused failure, but it was not failed (status: #{result.status})"
660
- end
661
- end
662
-
663
- failure_message_when_negated do |_result|
664
- "expected result not to have caused failure, but it did"
665
- end
666
-
667
- description do
668
- "have caused failure"
669
- end
670
- end
671
-
672
- # Tests that a task result represents a failure that was thrown from another task
673
- #
674
- # This matcher verifies that a failed result was propagated from another task
675
- # using CMDx's failure throwing mechanism. Optionally validates the original
676
- # result that was thrown.
677
- #
678
- # @param [CMDx::Result, nil] expected_original_result
679
- # Optional original result that should have been thrown
680
- #
681
- # @example Basic thrown failure validation
682
- # expect(result).to have_thrown_failure
683
- #
684
- # @example Thrown failure with specific original result
685
- # expect(result).to have_thrown_failure(original_failed_result)
686
- #
687
- # @example Negated usage (for caused failures)
688
- # expect(result).not_to have_thrown_failure
689
- #
690
- # @return [Boolean] true if result failed and threw a failure from another task
691
- #
692
- # @since 1.0.0
693
- RSpec::Matchers.define :have_thrown_failure do |expected_original_result = nil|
694
- match do |result|
695
- result.failed? &&
696
- result.threw_failure? &&
697
- (expected_original_result.nil? || result.threw_failure == expected_original_result)
698
- end
699
-
700
- failure_message do |result|
701
- messages = []
702
- messages << "expected result to be failed, but was #{result.status}" unless result.failed?
703
- messages << "expected result to have thrown failure, but it #{result.caused_failure? ? 'caused' : 'received'} failure instead" unless result.threw_failure?
704
-
705
- messages << "expected to throw failure from #{expected_original_result}, but threw from #{result.threw_failure}" if expected_original_result && result.threw_failure != expected_original_result
706
-
707
- messages.join(", ")
708
- end
709
-
710
- failure_message_when_negated do |_result|
711
- "expected result not to have thrown failure, but it did"
712
- end
713
-
714
- description do
715
- desc = "have thrown failure"
716
- desc += " from #{expected_original_result}" if expected_original_result
717
- desc
718
- end
719
- end
720
-
721
- # Tests that a task result represents a failure that was received from a thrown failure
722
- #
723
- # This matcher verifies that a failed result received a failure that was thrown
724
- # from another task in the execution chain. This is the receiving side of the
725
- # failure propagation mechanism.
726
- #
727
- # @example Basic received thrown failure validation
728
- # expect(result).to have_received_thrown_failure
729
- #
730
- # @example Negated usage
731
- # expect(result).not_to have_received_thrown_failure
732
- #
733
- # @return [Boolean] true if result failed and received a thrown failure
734
- #
735
- # @since 1.0.0
736
- RSpec::Matchers.define :have_received_thrown_failure do
737
- match do |result|
738
- result.failed? && result.thrown_failure?
739
- end
740
-
741
- failure_message do |result|
742
- if result.failed?
743
- "expected result to have received thrown failure, but it #{result.caused_failure? ? 'caused' : 'threw'} failure instead"
744
- else
745
- "expected result to have received thrown failure, but it was not failed (status: #{result.status})"
746
- end
747
- end
748
-
749
- failure_message_when_negated do |_result|
750
- "expected result not to have received thrown failure, but it did"
751
- end
752
-
753
- description do
754
- "have received thrown failure"
755
- end
756
- end
757
-
758
- # Tests that a task result belongs to a specific chain
759
- #
760
- # This matcher verifies that a result is associated with a CMDx::Chain
761
- # instance, optionally validating it's a specific chain object.
762
- #
763
- # @param [CMDx::Chain, nil] expected_chain
764
- # Optional specific chain instance to validate against
765
- #
766
- # @example Basic chain membership validation
767
- # expect(result).to belong_to_chain
768
- #
769
- # @example Specific chain validation
770
- # expect(result).to belong_to_chain(my_chain)
771
- #
772
- # @example Negated usage
773
- # expect(result).not_to belong_to_chain
774
- #
775
- # @return [Boolean] true if result belongs to a chain (optionally specific one)
776
- #
777
- # @since 1.0.0
778
- RSpec::Matchers.define :belong_to_chain do |expected_chain = nil|
779
- match do |result|
780
- result.chain.is_a?(CMDx::Chain) &&
781
- (expected_chain.nil? || result.chain == expected_chain)
782
- end
783
-
784
- failure_message do |result|
785
- if result.chain.is_a?(CMDx::Chain)
786
- "expected result to belong to chain #{expected_chain}, but belonged to #{result.chain}"
787
- else
788
- "expected result to belong to a chain, but chain was #{result.chain.class}"
789
- end
790
- end
791
-
792
- failure_message_when_negated do |_result|
793
- if expected_chain
794
- "expected result not to belong to chain #{expected_chain}, but it did"
795
- else
796
- "expected result not to belong to a chain, but it did"
797
- end
798
- end
799
-
800
- description do
801
- desc = "belong to chain"
802
- desc += " #{expected_chain}" if expected_chain
803
- desc
804
- end
805
- end
806
-
807
- # Tests that a task result has a specific chain index
808
- #
809
- # This matcher verifies that a result has the expected position index
810
- # within its chain, useful for testing chain execution order and position.
811
- #
812
- # @param [Integer] expected_index The expected chain index (0-based)
813
- #
814
- # @example Basic chain index validation
815
- # expect(result).to have_chain_index(0) # First task in chain
816
- # expect(result).to have_chain_index(2) # Third task in chain
817
- #
818
- # @example Negated usage
819
- # expect(result).not_to have_chain_index(1)
820
- #
821
- # @return [Boolean] true if result has the expected chain index
822
- #
823
- # @since 1.0.0
824
- RSpec::Matchers.define :have_chain_index do |expected_index|
825
- match do |result|
826
- result.index == expected_index
827
- end
828
-
829
- failure_message do |result|
830
- "expected result to have chain index #{expected_index}, but was #{result.index}"
831
- end
832
-
833
- failure_message_when_negated do |_result|
834
- "expected result not to have chain index #{expected_index}, but it did"
835
- end
836
-
837
- description do
838
- "have chain index #{expected_index}"
839
- end
840
- end
841
-
842
- # Auto-generated predicate matchers for common result states
843
- #
844
- # These matchers are dynamically created for each result state and provide
845
- # convenient boolean testing without requiring the full state name.
846
- #
847
- # Generated matchers:
848
- # - be_initialized: Tests if result is initialized
849
- # - be_executing: Tests if result is executing
850
- # - be_complete: Tests if result is complete
851
- # - be_interrupted: Tests if result is interrupted
852
- #
853
- # @example State predicate usage
854
- # expect(result).to be_complete
855
- # expect(result).to be_interrupted
856
- # expect(result).not_to be_initialized
857
- #
858
- # @since 1.0.0
859
- CMDx::Result::STATES.each do |state|
860
- RSpec::Matchers.define :"be_#{state}" do
861
- match do |result|
862
- result.public_send(:"#{state}?")
863
- end
864
-
865
- failure_message do |result|
866
- "expected result to be #{state}, but was #{result.state}"
867
- end
868
-
869
- failure_message_when_negated do |_result|
870
- "expected result not to be #{state}, but it was"
871
- end
872
-
873
- description do
874
- "be #{state}"
875
- end
876
- end
877
- end
878
-
879
- # Auto-generated predicate matchers for common result statuses
880
- #
881
- # These matchers are dynamically created for each result status and provide
882
- # convenient boolean testing without requiring status-specific matchers.
883
- #
884
- # Generated matchers:
885
- # - be_success: Tests if result has success status
886
- # - be_skipped: Tests if result has skipped status
887
- # - be_failed: Tests if result has failed status
888
- #
889
- # @example Status predicate usage
890
- # expect(result).to be_success
891
- # expect(result).to be_failed
892
- # expect(result).not_to be_skipped
893
- #
894
- # @note These are simpler alternatives to the full outcome matchers
895
- # (be_successful_task, be_failed_task, be_skipped_task) when you only
896
- # need to test the status without additional validation.
897
- #
898
- # @since 1.0.0
899
- CMDx::Result::STATUSES.each do |status|
900
- RSpec::Matchers.define :"be_#{status}" do
901
- match do |result|
902
- result.public_send(:"#{status}?")
903
- end
904
-
905
- failure_message do |result|
906
- "expected result to be #{status}, but was #{result.status}"
907
- end
908
-
909
- failure_message_when_negated do |_result|
910
- "expected result not to be #{status}, but it was"
911
- end
912
-
913
- description do
914
- "be #{status}"
915
- end
916
- end
917
- end