cmdx-rspec 1.3.0 → 2.0.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +279 -156
  4. data/lib/cmdx/rspec/helpers.rb +257 -259
  5. data/lib/cmdx/rspec/matchers/be_complete.rb +20 -0
  6. data/lib/cmdx/rspec/matchers/be_deprecated.rb +14 -52
  7. data/lib/cmdx/rspec/matchers/be_interrupted.rb +20 -0
  8. data/lib/cmdx/rspec/matchers/be_ko.rb +19 -0
  9. data/lib/cmdx/rspec/matchers/be_ok.rb +19 -0
  10. data/lib/cmdx/rspec/matchers/be_successful.rb +8 -20
  11. data/lib/cmdx/rspec/matchers/have_been_retried.rb +41 -0
  12. data/lib/cmdx/rspec/matchers/have_been_rolled_back.rb +23 -0
  13. data/lib/cmdx/rspec/matchers/have_callback.rb +46 -0
  14. data/lib/cmdx/rspec/matchers/have_chain_root.rb +30 -0
  15. data/lib/cmdx/rspec/matchers/have_chain_size.rb +29 -0
  16. data/lib/cmdx/rspec/matchers/have_duration.rb +30 -0
  17. data/lib/cmdx/rspec/matchers/have_empty_context.rb +4 -16
  18. data/lib/cmdx/rspec/matchers/have_empty_metadata.rb +3 -9
  19. data/lib/cmdx/rspec/matchers/have_errors_on.rb +50 -0
  20. data/lib/cmdx/rspec/matchers/have_failed.rb +7 -24
  21. data/lib/cmdx/rspec/matchers/have_input.rb +41 -0
  22. data/lib/cmdx/rspec/matchers/have_matching_context.rb +5 -20
  23. data/lib/cmdx/rspec/matchers/have_matching_metadata.rb +5 -17
  24. data/lib/cmdx/rspec/matchers/have_middleware.rb +29 -0
  25. data/lib/cmdx/rspec/matchers/have_no_errors.rb +30 -0
  26. data/lib/cmdx/rspec/matchers/have_output.rb +46 -0
  27. data/lib/cmdx/rspec/matchers/have_pipeline_tasks.rb +27 -0
  28. data/lib/cmdx/rspec/matchers/have_retry_on.rb +36 -0
  29. data/lib/cmdx/rspec/matchers/have_skipped.rb +7 -24
  30. data/lib/cmdx/rspec/matchers/have_tag.rb +30 -0
  31. data/lib/cmdx/rspec/matchers/raise_cmdx_fault.rb +74 -0
  32. data/lib/cmdx/rspec/version.rb +2 -1
  33. data/lib/cmdx/rspec.rb +22 -3
  34. metadata +24 -15
  35. data/.cursor/prompts/docs.md +0 -12
  36. data/.cursor/prompts/llms.md +0 -20
  37. data/.cursor/prompts/rspec.md +0 -24
  38. data/.cursor/prompts/yardoc.md +0 -14
  39. data/.cursor/rules/cursor-instructions.mdc +0 -62
  40. data/.rspec +0 -4
  41. data/.rubocop.yml +0 -64
  42. data/src/cmdx-dark-logo.png +0 -0
  43. data/src/cmdx-light-logo.png +0 -0
  44. data/src/cmdx-logo.svg +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4fc7ba1c5a520fd78933d838b99697a1aeb3e13aab9383f430ca22b3c227b6de
4
- data.tar.gz: 4feb370c926b26b7499e51cf547bca870edc0ebff1184c4431b9fbf107258493
3
+ metadata.gz: 9d99809d55db6542a630f55457df90bacbf7598a9969a8a1e54cf27630362030
4
+ data.tar.gz: 7fe0770bb36f09002b9b45cbf01751b0aab0efac7529be7c3c65d5a751399dbb
5
5
  SHA512:
6
- metadata.gz: 4656c4a7f6cd3a4d4c111edcc6d8cee2fe26049f9925d195734a13e8e9bf63c3dd03cbe60a3805e6bcb47c8e4fa327e60f24cd2f7970d07cc225cec702172127
7
- data.tar.gz: 6f0ceb3dca15652a1dbdff3ee6f06cf0530b7562365fa98ac698f4618310ab5bff2f6c0a2ce83b88b8ed443115dbfc7f8194eb7fb7498316ce27317041c0040d
6
+ metadata.gz: 3fa228ad4eec439aea347ed0c12746f66252e0712dfba58ddd246ffde63102dcedc5c2b2c5816441f9d5eec9a3261937ad19ea22c31b197c74b7e20f5daa8cdc
7
+ data.tar.gz: 5818b4c8a34414a5834abb7e8665480d24b9c0e63ae1d5bebc2f1fa37c5156b9b377ec26cef78251a24c1ce2bd98b4d8f439031aa4ba8efb52a9b8888c903c91
data/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [2.0.0] - Unreleased
10
+
11
+ ### Added
12
+ - Support for CMDx v2
13
+
14
+ ### Changes
15
+ - Updated helpers and matchers for new internals
16
+
17
+ ## [1.4.0] - 2026-04-09
18
+
19
+ ### Added
20
+ - Added `stub_workflow_tasks` helper
21
+
9
22
  ## [1.3.0] - 2025-11-09
10
23
 
11
24
  ### Added
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ---
6
6
 
7
- Collection of RSpec matchers for the CMDx framework.
7
+ Collection of RSpec matchers and helpers for the CMDx framework.
8
8
 
9
9
  [Changelog](./CHANGELOG.md) · [Report Bug](https://github.com/drexed/cmdx-rspec/issues) · [Request Feature](https://github.com/drexed/cmdx-rspec/issues)
10
10
 
@@ -15,141 +15,267 @@
15
15
 
16
16
  # CMDx::RSpec
17
17
 
18
- Collection of RSpec matchers for [CMDx](https://github.com/drexed/cmdx).
18
+ RSpec matchers and helpers for asserting [CMDx](https://github.com/drexed/cmdx) task and workflow behavior — result state, errors, faults, callbacks, retries, chains, and more — without invoking real `work` blocks.
19
+
20
+ ## Requirements
21
+
22
+ - Ruby: MRI 3.3+ or a compatible JRuby/TruffleRuby release
23
+ - CMDx 2.0+
19
24
 
20
25
  ## Installation
21
26
 
22
- Add this line to your application's Gemfile:
27
+ ```sh
28
+ gem install cmdx-rspec
29
+ # - or -
30
+ bundle add cmdx-rspec --group test
31
+ ```
32
+
33
+ Require the library in `spec_helper.rb` (or equivalent):
23
34
 
24
35
  ```ruby
25
- gem 'cmdx-rspec'
36
+ require "cmdx/rspec"
26
37
  ```
27
38
 
28
- And then execute:
39
+ This loads every matcher under `RSpec::Matchers` and exposes the helpers under `CMDx::RSpec::Helpers`. See [Helpers](#helpers) for how to mix the helpers into your example groups.
29
40
 
30
- $ bundle
41
+ ## Matchers
31
42
 
32
- Or install it yourself as:
43
+ All result-oriented matchers raise `ArgumentError` when given a subject that isn't a `CMDx::Result`. Class-oriented matchers accept either the Task class or an instance.
33
44
 
34
- $ gem install cmdx-rspec
45
+ ### Result state & status
35
46
 
36
- ## Matchers
47
+ #### `be_successful`
37
48
 
38
- ### be_successful
49
+ Asserts a `CMDx::Result` completed with `state: complete` and `status: success`. Extra keyword args are forwarded to a `result.to_h` inclusion check, so any field can be constrained inline.
39
50
 
40
- Asserts that a CMDx task result indicates successful execution.
51
+ ```ruby
52
+ expect(SomeTask.execute).to be_successful
53
+ expect(SomeTask.execute).to be_successful(metadata: { id: 1 })
54
+ ```
55
+
56
+ #### `have_skipped`
57
+
58
+ Asserts a result was skipped (`state: interrupted`, `status: skipped`). Extra keyword args constrain other `result.to_h` fields.
41
59
 
42
60
  ```ruby
43
- it "returns success" do
44
- result = SomeTask.execute
61
+ expect(result).to have_skipped
62
+ expect(result).to have_skipped(
63
+ reason: "out of stock",
64
+ cause: be_a(CMDx::SkipFault)
65
+ )
66
+ ```
45
67
 
46
- expect(result).to be_successful
47
- end
68
+ #### `have_failed`
69
+
70
+ Asserts a result failed (`state: interrupted`, `status: failed`). Extra keyword args constrain other `result.to_h` fields.
71
+
72
+ ```ruby
73
+ expect(result).to have_failed
74
+ expect(result).to have_failed(
75
+ reason: "boom",
76
+ cause: be_a(NoMethodError)
77
+ )
48
78
  ```
49
79
 
50
- ### have_skipped
80
+ #### `be_ok` / `be_ko`
51
81
 
52
- Asserts that a CMDx task result indicates the task was skipped during execution.
82
+ `be_ok` passes when the result is success or skipped (anything but failed). `be_ko` is its inverse.
53
83
 
54
84
  ```ruby
55
- it "returns skipped" do
56
- result = SomeTask.execute
85
+ expect(result).to be_ok
86
+ expect(result).to be_ko
87
+ ```
57
88
 
58
- # Default result
59
- expect(result).to have_skipped
89
+ #### `be_complete` / `be_interrupted`
60
90
 
61
- # Custom result
62
- expect(result).to have_skipped(
63
- reason: "Skipping for a custom reason",
64
- cause: be_a(CMDx::SkipFault)
65
- # Other members of `result.to_h`...
66
- )
67
- end
91
+ State-only assertions. `be_complete` passes when `state == :complete`; `be_interrupted` when `state == :interrupted`.
92
+
93
+ ```ruby
94
+ expect(result).to be_complete
95
+ expect(result).to be_interrupted
68
96
  ```
69
97
 
70
- ### have_failed
98
+ ### Result data
71
99
 
72
- Asserts that a CMDx task result indicates execution failure.
100
+ #### `have_empty_metadata` / `have_matching_metadata`
101
+
102
+ `have_empty_metadata` requires `metadata` to be empty. `have_matching_metadata` performs a partial-hash inclusion match (and delegates to `have_empty_metadata` when called with no args).
73
103
 
74
104
  ```ruby
75
- it "returns failure" do
76
- result = SomeTask.execute
105
+ expect(result).to have_empty_metadata
106
+ expect(result).to have_matching_metadata(status_code: 500)
107
+ ```
77
108
 
78
- # Default result
79
- expect(result).to have_failed
109
+ #### `have_empty_context` / `have_matching_context`
80
110
 
81
- # Custom result
82
- expect(result).to have_failed(
83
- reason: "Failed for a custom reason",
84
- cause: be_a(NoMethodError)
85
- # Other members of `result.to_h`...
86
- )
87
- end
111
+ Same pattern for the result's context. Both accept a `Hash`, `CMDx::Context`, or `CMDx::Result` (the result's `.context` is unwrapped automatically).
112
+
113
+ ```ruby
114
+ expect(result).to have_empty_context
115
+ expect(result).to have_matching_context(stored_id: 123)
88
116
  ```
89
117
 
90
- ### have_empty_metadata
118
+ ### Errors
91
119
 
92
- Asserts that a CMDx task result has no metadata.
120
+ #### `have_no_errors`
121
+
122
+ Passes when the subject's `errors` collection is empty. Accepts a `CMDx::Result`, `CMDx::Task` instance, or `CMDx::Errors`.
93
123
 
94
124
  ```ruby
95
- it "returns empty metadata" do
96
- result = SomeTask.execute
125
+ expect(result).to have_no_errors
126
+ ```
97
127
 
98
- expect(result).to have_empty_metadata
99
- end
128
+ #### `have_errors_on`
129
+
130
+ Asserts at least one error is present under `key`. Optional positional `messages` further constrain the matcher — all must be present.
131
+
132
+ ```ruby
133
+ expect(result).to have_errors_on(:email)
134
+ expect(result).to have_errors_on(:email, "is required")
135
+ expect(task).to have_errors_on(:email, "is required", "is invalid")
100
136
  ```
101
137
 
102
- ### have_matching_metadata
138
+ ### Execution metrics
139
+
140
+ #### `have_been_retried`
103
141
 
104
- Asserts that a CMDx task result contains specific metadata.
142
+ Passes when the result was retried at least once. Pass an integer to require an exact retry count.
105
143
 
106
144
  ```ruby
107
- it "returns matching metadata" do
108
- result = SomeTask.execute
145
+ expect(result).to have_been_retried
146
+ expect(result).to have_been_retried(3)
147
+ ```
109
148
 
110
- expect(result).to have_matching_metadata(status_code: 500)
111
- end
149
+ #### `have_been_rolled_back`
150
+
151
+ Passes when a failing task ran its rollback hook.
152
+
153
+ ```ruby
154
+ expect(result).to have_been_rolled_back
112
155
  ```
113
156
 
114
- ### have_empty_context
157
+ #### `have_duration`
115
158
 
116
- Asserts that a CMDx task result has no context data.
159
+ Asserts the result's duration (in milliseconds) falls within the supplied bounds. At least one of `:less_than` or `:greater_than` is required.
117
160
 
118
161
  ```ruby
119
- it "returns empty context" do
120
- result = SomeTask.execute
162
+ expect(result).to have_duration(less_than: 100)
163
+ expect(result).to have_duration(greater_than: 0.1, less_than: 50)
164
+ ```
121
165
 
122
- expect(result).to have_empty_context
123
- end
166
+ ### Faults
167
+
168
+ #### `raise_cmdx_fault`
169
+
170
+ Block matcher that asserts a `CMDx::Fault` is raised. Optionally constrain by originating task class, reason, or underlying cause.
171
+
172
+ ```ruby
173
+ expect { SomeTask.execute! }.to raise_cmdx_fault
174
+ expect { SomeTask.execute! }.to raise_cmdx_fault(SomeTask)
175
+ expect { SomeTask.execute! }
176
+ .to raise_cmdx_fault(SomeTask)
177
+ .with_reason(/invalid/)
178
+ .with_cause(MyError)
179
+ ```
180
+
181
+ `with_reason` accepts a string (equality) or a Regexp; `with_cause` accepts a class (matches via `is_a?`) or a value (matched with `values_match?`).
182
+
183
+ ### Chains
184
+
185
+ #### `have_chain_root`
186
+
187
+ Passes when the chain's root task class matches (or is a subclass of) `task_class`. Accepts a `CMDx::Chain` or `CMDx::Result`.
188
+
189
+ ```ruby
190
+ expect(result).to have_chain_root(MyWorkflow)
124
191
  ```
125
192
 
126
- ### have_matching_context
193
+ #### `have_chain_size`
127
194
 
128
- Asserts that a CMDx task result contains specific context data.
195
+ Passes when the chain's size matches `expected`. Accepts a `CMDx::Chain` or `CMDx::Result`.
129
196
 
130
197
  ```ruby
131
- it "returns matching context" do
132
- result = SomeTask.execute
198
+ expect(result).to have_chain_size(3)
199
+ ```
133
200
 
134
- expect(result).to have_matching_context(stored_result: 123)
135
- end
201
+ ### Task class declarations
202
+
203
+ These matchers introspect a Task class's configuration. They accept either the class or an instance.
204
+
205
+ #### `be_deprecated`
206
+
207
+ Asserts a Task class is marked deprecated. Optionally constrain the deprecation behavior via a positional value or a chained convenience method.
208
+
209
+ ```ruby
210
+ expect(SomeTask).to be_deprecated
211
+ expect(SomeTask).to be_deprecated.with_warning # :warn
212
+ expect(SomeTask).to be_deprecated.with_logging # :log
213
+ expect(SomeTask).to be_deprecated.with_error # :error
214
+ expect(SomeTask).to be_deprecated.with_behavior(:custom)
136
215
  ```
137
216
 
138
- ### be_deprecated
217
+ #### `have_input` / `have_output`
139
218
 
140
- Asserts that a CMDx task result indicates the task is deprecated.
219
+ Asserts the class declares the given input/output. Keyword args are matched (partial) against the parameter's serialized `to_h`.
141
220
 
142
221
  ```ruby
143
- it "returns deprecated" do
144
- expect(SomeTask).to be_deprecated
145
- end
222
+ expect(SomeTask).to have_input(:user_id)
223
+ expect(SomeTask).to have_input(:user_id, type: :integer, required: true)
224
+ expect(SomeTask).to have_output(:total, required: true)
225
+ ```
226
+
227
+ #### `have_callback`
228
+
229
+ Asserts a callback is registered for `event`. Optional `callable` further constrains the match — by `==` for symbols/lambdas, or by `is_a?` when given a class.
230
+
231
+ ```ruby
232
+ expect(SomeTask).to have_callback(:before_execution)
233
+ expect(SomeTask).to have_callback(:before_execution, :authenticate!)
234
+ expect(SomeTask).to have_callback(:on_failed, AlertOnFailure)
235
+ ```
236
+
237
+ #### `have_middleware`
238
+
239
+ Asserts the class registered `middleware`. Class arguments match by `is_a?` or `==`; other values match by `==`.
240
+
241
+ ```ruby
242
+ expect(SomeTask).to have_middleware(LoggingMiddleware)
243
+ ```
244
+
245
+ #### `have_retry_on`
246
+
247
+ Asserts the class is configured to retry on `exception`. Keyword args check `CMDx::Retry` configuration values (`:limit`, `:delay`, `:max_delay`, `:jitter`).
248
+
249
+ ```ruby
250
+ expect(SomeTask).to have_retry_on(Net::OpenTimeout)
251
+ expect(SomeTask).to have_retry_on(Net::OpenTimeout, limit: 5, jitter: :exponential)
252
+ ```
253
+
254
+ #### `have_tag`
255
+
256
+ Asserts the subject carries `tag`. Accepts a Task class (reads `settings.tags`) or a `CMDx::Result` (reads `result.tags`).
257
+
258
+ ```ruby
259
+ expect(SomeTask).to have_tag(:critical)
260
+ expect(result).to have_tag(:critical)
261
+ ```
262
+
263
+ ### Workflows
264
+
265
+ #### `have_pipeline_tasks`
266
+
267
+ Asserts a `CMDx::Workflow` class declares the given pipeline tasks. Order-sensitive by default; chain `.in_any_order` for set comparison.
268
+
269
+ ```ruby
270
+ expect(MyWorkflow).to have_pipeline_tasks(StepA, StepB, StepC)
271
+ expect(MyWorkflow).to have_pipeline_tasks(StepA, StepC, StepB).in_any_order
146
272
  ```
147
273
 
148
274
  ## Helpers
149
275
 
150
- ### Including Helper Modules
276
+ ### Including helper modules
151
277
 
152
- Include the helper modules in your RSpec configuration or example groups:
278
+ Mix into all example groups via RSpec config, or include in specific groups:
153
279
 
154
280
  ```ruby
155
281
  RSpec.configure do |config|
@@ -157,140 +283,137 @@ RSpec.configure do |config|
157
283
  end
158
284
  ```
159
285
 
160
- Or include them in specific example groups:
161
-
162
286
  ```ruby
163
287
  describe MyFeature do
164
288
  include CMDx::RSpec::Helpers
165
-
166
- # Your specs...
289
+ # ...
167
290
  end
168
291
  ```
169
292
 
170
293
  ### Stubs
171
294
 
172
- Helper methods for stubbing CMDx command execution.
295
+ Each stub builds a frozen `CMDx::Result` carrying the requested signal and wires it into a fresh `CMDx::Chain`, so callers see realistic execution shape without invoking the task's `work`. Any extra keyword args (besides the documented ones) are forwarded to `command.new` as context overrides.
173
296
 
174
- #### Types
297
+ #### Result-type stubs
175
298
 
176
299
  ```ruby
177
- it "stubs task executions by type" do
178
- # eg: SomeTask.execute
179
- stub_task_success(SomeTask)
180
- stub_task_skip(SomeTask)
181
- stub_task_fail(SomeTask)
182
-
183
- # eg: SomeTask.execute!
184
- stub_task_success!(SomeTask)
185
- stub_task_skip!(SomeTask)
186
- stub_task_fail!(SomeTask)
187
-
188
- # Your specs...
189
- end
190
-
191
- it "stubs task with arguments" do
192
- # eg: SomeTask.execute(some: "value")
193
- stub_task_success(SomeTask, some: "value")
300
+ # Non-bang variants stub `SomeTask.execute`
301
+ stub_task_success(SomeTask)
302
+ stub_task_skip(SomeTask)
303
+ stub_task_fail(SomeTask)
304
+
305
+ # Bang variants stub `SomeTask.execute!`
306
+ stub_task_success!(SomeTask)
307
+ stub_task_skip!(SomeTask)
308
+ stub_task_fail!(SomeTask)
309
+
310
+ # Stub a specific argument signature
311
+ stub_task_success(SomeTask, some: "value") # SomeTask.execute(some: "value")
312
+ stub_task_skip!(SomeTask, some: "value") # SomeTask.execute!(some: "value")
313
+ ```
194
314
 
195
- # eg: SomeTask.execute!(some: "value")
196
- stub_task_skip!(SomeTask, some: "value")
315
+ Common options:
197
316
 
198
- # Your specs...
199
- end
317
+ ```ruby
318
+ stub_task_success(SomeTask, metadata: { id: 1 })
319
+ stub_task_skip!(SomeTask, reason: "out of stock")
320
+ stub_task_fail!(SomeTask, cause: NoMethodError.new("boom"))
200
321
  ```
201
322
 
202
- #### Options
323
+ #### Specialized stubs
203
324
 
204
325
  ```ruby
205
- it "stubs task with metadata" do
206
- stub_task_success(SomeTask, metadata: { some: "value" })
326
+ # Models the rescued StandardError -> failed signal path that Runtime
327
+ # takes when `work` raises something other than a Fault.
328
+ stub_task_error(SomeTask, Net::OpenTimeout, "boom")
207
329
 
208
- # Your specs...
209
- end
330
+ # Models the `throw!`-then-propagate path used by nested tasks/workflows.
331
+ # `upstream_result` must be a failed CMDx::Result.
332
+ stub_task_throw(SomeTask, upstream_result)
210
333
 
211
- it "stubs task with a custom reason" do
212
- stub_task_skip!(SomeTask, reason: "Skipping for a custom reason")
334
+ # Returns a successful Result flagged as `deprecated?` without triggering
335
+ # the real Deprecation action.
336
+ stub_task_deprecated(SomeTask)
337
+ ```
213
338
 
214
- # Your specs...
215
- end
339
+ #### Workflow stubs
216
340
 
217
- it "stubs task with a custom cause" do
218
- stub_task_fail!(SomeTask, cause: NoMethodError.new("just blow it up"))
341
+ `stub_workflow_tasks` yields each distinct Task class reachable from a Workflow's pipeline (first-seen order) so you can stub them in one place.
219
342
 
220
- # Your specs...
343
+ ```ruby
344
+ stub_workflow_tasks(MyWorkflow) do |task|
345
+ case task
346
+ when TaskC then stub_task_skip(task)
347
+ else stub_task_success(task)
348
+ end
221
349
  end
350
+
351
+ MyWorkflow.execute
222
352
  ```
223
353
 
224
- #### Reset
354
+ #### Unstubbing
225
355
 
226
- ```ruby
227
- it "unstubs task executions by type" do
228
- # eg: SomeTask.execute
229
- unstub_task(SomeTask)
356
+ Restores the original implementation. When `context` is supplied, only that argument signature is unstubbed.
230
357
 
231
- # eg: SomeTask.execute!
232
- unstub_task!(SomeTask)
358
+ ```ruby
359
+ unstub_task(SomeTask) # SomeTask.execute
360
+ unstub_task!(SomeTask) # SomeTask.execute!
361
+ unstub_task(SomeTask, some: "value") # SomeTask.execute(some: "value")
362
+ ```
233
363
 
234
- # Your specs...
235
- end
364
+ ### Mocks
236
365
 
237
- it "unstubs task with arguments" do
238
- # eg: SomeTask.execute(some: "value")
239
- unstub_task(SomeTask, some: "value")
366
+ Message expectations on `execute` / `execute!`. When `context` is supplied, the expectation is constrained to that signature.
240
367
 
241
- # eg: SomeTask.execute!(some: "value")
242
- unstub_task!(SomeTask, some: "value")
368
+ ```ruby
369
+ expect_task_execution(SomeTask)
370
+ expect_task_execution!(SomeTask)
371
+ expect_task_execution(SomeTask, some: "value")
243
372
 
244
- # Your specs...
245
- end
373
+ expect_no_task_execution(SomeTask)
374
+ expect_no_task_execution!(SomeTask)
375
+ expect_no_task_execution(SomeTask, some: "value")
246
376
  ```
247
377
 
248
- ### Mocks
378
+ ### Diagnostics
249
379
 
250
- Helper methods for setting expectations on CMDx command execution.
380
+ #### `capture_cmdx_logs`
251
381
 
252
- #### Types
382
+ Captures lines written to a temporary `CMDx.configuration.logger` for the duration of the block. The previous logger is restored on exit.
253
383
 
254
384
  ```ruby
255
- it "mocks task executions by type" do
256
- # eg: SomeTask.execute
257
- expect_task_execution(SomeTask)
258
- expect_no_task_execution(SomeTask)
385
+ logs = capture_cmdx_logs { MyCommand.execute }
386
+ expect(logs.join).to include("status=success")
387
+ ```
259
388
 
260
- # eg: SomeTask.execute!
261
- expect_task_execution!(BangCommand)
262
- expect_no_task_execution!(SomeTask)
389
+ #### `subscribe_telemetry`
263
390
 
264
- # Your specs...
265
- end
391
+ Subscribes to telemetry events on a Task's telemetry registry for the duration of the block and returns every emitted event in order. Tasks subclassing `command` also fire (the registry is shared by reference until `dup`). Defaults to all events in `CMDx::Telemetry::EVENTS`.
266
392
 
267
- it "mocks task with arguments" do
268
- # eg: SomeTask.execute(some: "value")
269
- expect_task_execution(SomeTask, some: "value")
270
- expect_no_task_execution(SomeTask, some: "value")
393
+ ```ruby
394
+ events = subscribe_telemetry(MyCommand, :task_executed) { MyCommand.execute }
395
+ expect(events.map(&:name)).to eq([:task_executed])
396
+ ```
271
397
 
272
- # eg: SomeTask.execute!(some: "value")
273
- expect_task_execution!(SomeTask, some: "value")
274
- expect_no_task_execution!(SomeTask, some: "value")
398
+ #### `with_cmdx_chain`
275
399
 
276
- # Your specs...
277
- end
400
+ Captures the `CMDx::Chain` produced by the first root execution of `command` within the block. Returns `nil` if `command` didn't run as a root.
401
+
402
+ ```ruby
403
+ chain = with_cmdx_chain(MyWorkflow) { MyWorkflow.execute }
404
+ expect(chain.size).to be > 1
278
405
  ```
279
406
 
280
407
  ## Development
281
408
 
282
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
409
+ Run `bin/setup` to install dependencies, then `rake spec` to run the tests. Use `bin/console` for an interactive prompt.
283
410
 
284
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
411
+ Release flow: bump `lib/cmdx/rspec/version.rb`, then `bundle exec rake release` to tag, push, and publish to [rubygems.org](https://rubygems.org).
285
412
 
286
413
  ## Contributing
287
414
 
288
- Bug reports and pull requests are welcome on GitHub at https://github.com/drexed/cmdx-rspec. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/drexed/cmdx-rspec/blob/master/CODE_OF_CONDUCT.md).
415
+ Bug reports and pull requests are welcome at <https://github.com/drexed/cmdx-rspec>. Contributors are expected to follow the [code of conduct](https://github.com/drexed/cmdx-rspec/blob/master/CODE_OF_CONDUCT.md).
289
416
 
290
417
  ## License
291
418
 
292
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
293
-
294
- ## Code of Conduct
295
-
296
- Everyone interacting in the Cmdx::Rspec project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/drexed/cmdx-rspec/blob/master/CODE_OF_CONDUCT.md).
419
+ Released under the [MIT License](https://opensource.org/licenses/MIT).