Module: CMDx::RSpec::Helpers

Defined in:
lib/cmdx/rspec/helpers.rb

Overview

RSpec helpers for stubbing and asserting Task execution. Each helper builds a frozen CMDx::Result carrying the requested Signal and wires it into a fresh Chain so callers see realistic execution output without invoking the Task’s ‘work`.

Mix into example groups via ‘config.include CMDx::RSpec::Helpers`.

Instance Method Summary collapse

Instance Method Details

#capture_cmdx_logs { ... } ⇒ Array<String>

Captures lines written to a temporary CMDx logger for the duration of the block. Restores the previous logger on exit.

Examples:

logs = capture_cmdx_logs { MyCommand.execute }
expect(logs.join).to include("status=success")

Yields:

  • runs the block with ‘CMDx.configuration.logger` swapped

Returns:

  • (Array<String>)

    captured log lines



141
142
143
144
145
146
147
148
149
150
151
# File 'lib/cmdx/rspec/helpers.rb', line 141

def capture_cmdx_logs(&)
  raise ArgumentError, "block required" unless block_given?

  io = StringIO.new
  previous = CMDx.configuration.logger
  CMDx.configuration.logger = Logger.new(io, formatter: previous&.formatter || CMDx::LogFormatters::Line.new)
  yield
  io.string.lines.map(&:chomp)
ensure
  CMDx.configuration.logger = previous if previous
end

#expect_no_task_execution(command, **context) ⇒ RSpec::Mocks::MessageExpectation

Sets a negative message expectation that ‘command.execute` is not invoked. When `context` is supplied, only the matching signature is forbidden.

Parameters:

  • command (Class)

    the Task class to guard

  • context (Hash{Symbol => Object})

    argument signature to forbid

Returns:

  • (RSpec::Mocks::MessageExpectation)


278
279
280
281
282
283
284
# File 'lib/cmdx/rspec/helpers.rb', line 278

def expect_no_task_execution(command, **context)
  if context.empty?
    expect(command).not_to receive(:execute)
  else
    expect(command).not_to receive(:execute).with(**context)
  end
end

#expect_no_task_execution!(command, **context) ⇒ RSpec::Mocks::MessageExpectation

Sets a negative message expectation that ‘command.execute!` is not invoked. When `context` is supplied, only the matching signature is forbidden.

Parameters:

  • command (Class)

    the Task class to guard

  • context (Hash{Symbol => Object})

    argument signature to forbid

Returns:

  • (RSpec::Mocks::MessageExpectation)


292
293
294
295
296
297
298
# File 'lib/cmdx/rspec/helpers.rb', line 292

def expect_no_task_execution!(command, **context)
  if context.empty?
    expect(command).not_to receive(:execute!)
  else
    expect(command).not_to receive(:execute!).with(**context)
  end
end

#expect_task_execution(command, **context) ⇒ RSpec::Mocks::MessageExpectation

Sets a positive message expectation that ‘command.execute` is invoked. When `context` is supplied, the expectation is constrained to that signature.

Parameters:

  • command (Class)

    the Task class to expect

  • context (Hash{Symbol => Object})

    argument signature to match

Returns:

  • (RSpec::Mocks::MessageExpectation)


250
251
252
253
254
255
256
# File 'lib/cmdx/rspec/helpers.rb', line 250

def expect_task_execution(command, **context)
  if context.empty?
    expect(command).to receive(:execute)
  else
    expect(command).to receive(:execute).with(**context)
  end
end

#expect_task_execution!(command, **context) ⇒ RSpec::Mocks::MessageExpectation

Sets a positive message expectation that ‘command.execute!` is invoked. When `context` is supplied, the expectation is constrained to that signature.

Parameters:

  • command (Class)

    the Task class to expect

  • context (Hash{Symbol => Object})

    argument signature to match

Returns:

  • (RSpec::Mocks::MessageExpectation)


264
265
266
267
268
269
270
# File 'lib/cmdx/rspec/helpers.rb', line 264

def expect_task_execution!(command, **context)
  if context.empty?
    expect(command).to receive(:execute!)
  else
    expect(command).to receive(:execute!).with(**context)
  end
end

#stub_task_deprecated(command, metadata: {}, **context) ⇒ CMDx::Result

Stubs ‘command.execute` to return a successful Result flagged as `deprecated?`. Useful when asserting deprecation surfaces without triggering the real `Deprecation` action.

Parameters:

  • command (Class)

    the Task class to stub

  • metadata (Hash) (defaults to: {})
  • context (Hash{Symbol => Object})

Returns:

  • (CMDx::Result)


129
130
131
# File 'lib/cmdx/rspec/helpers.rb', line 129

def stub_task_deprecated(command, metadata: {}, **context)
  build_stub(command, :execute, CMDx::Signal.success(nil, metadata:), context, deprecated: true)
end

#stub_task_error(command, exception, message = nil, metadata: {}, **context) ⇒ CMDx::Result

Stubs ‘command.execute` to return a frozen failed Result whose `cause` is an instance of exception. Models the “rescued StandardError -> failed signal” path that Runtime takes when a task’s ‘work` raises something other than a Fault.

Examples:

stub_task_error(MyCommand, Net::OpenTimeout, "boom")

Parameters:

  • command (Class)

    the Task class to stub

  • exception (Class<StandardError>, StandardError)

    the cause

  • message (String, nil) (defaults to: nil)

    message used when constructing the exception (when exception is a Class) and to derive the reason

  • metadata (Hash) (defaults to: {})
  • context (Hash{Symbol => Object})

Returns:

  • (CMDx::Result)

    the frozen Result installed on the stub



97
98
99
100
101
# File 'lib/cmdx/rspec/helpers.rb', line 97

def stub_task_error(command, exception, message = nil, metadata: {}, **context)
  ex = exception.is_a?(Class) ? exception.new(message || "stubbed") : exception
  reason = "[#{ex.class}] #{ex.message}"
  build_stub(command, :execute, CMDx::Signal.failed(reason, metadata:, cause: ex), context)
end

#stub_task_fail(command, reason: nil, cause: nil, metadata: {}, **context) ⇒ CMDx::Result

Stubs ‘command.execute` to return a frozen failed Result.

Parameters:

  • command (Class)

    the Task class to stub

  • reason (String, nil) (defaults to: nil)

    human-readable failure reason

  • cause (Exception, nil) (defaults to: nil)

    originating cause attached to the signal

  • metadata (Hash) (defaults to: {})

    payload exposed via ‘result.metadata`

  • context (Hash{Symbol => Object})

    context overrides forwarded to ‘command.new`

Returns:

  • (CMDx::Result)

    the frozen Result installed on the stub



67
68
69
# File 'lib/cmdx/rspec/helpers.rb', line 67

def stub_task_fail(command, reason: nil, cause: nil, metadata: {}, **context)
  build_stub(command, :execute, CMDx::Signal.failed(reason, metadata:, cause:), context)
end

#stub_task_fail!(command, reason: nil, cause: nil, metadata: {}, **context) ⇒ CMDx::Result

Stubs ‘command.execute!` (bang variant) to return a frozen failed Result.

Parameters:

  • command (Class)

    the Task class to stub

  • reason (String, nil) (defaults to: nil)

    human-readable failure reason

  • cause (Exception, nil) (defaults to: nil)

    originating cause attached to the signal

  • metadata (Hash) (defaults to: {})

    payload exposed via ‘result.metadata`

  • context (Hash{Symbol => Object})

    context overrides forwarded to ‘command.new`

Returns:

  • (CMDx::Result)

    the frozen Result installed on the stub



79
80
81
# File 'lib/cmdx/rspec/helpers.rb', line 79

def stub_task_fail!(command, reason: nil, cause: nil, metadata: {}, **context)
  build_stub(command, :execute!, CMDx::Signal.failed(reason, metadata:, cause:), context, strict: true)
end

#stub_task_skip(command, reason: nil, cause: nil, metadata: {}, **context) ⇒ CMDx::Result

Stubs ‘command.execute` to return a frozen skipped Result.

Parameters:

  • command (Class)

    the Task class to stub

  • reason (String, nil) (defaults to: nil)

    human-readable skip reason

  • cause (Exception, nil) (defaults to: nil)

    originating cause attached to the signal

  • metadata (Hash) (defaults to: {})

    payload exposed via ‘result.metadata`

  • context (Hash{Symbol => Object})

    context overrides forwarded to ‘command.new`

Returns:

  • (CMDx::Result)

    the frozen Result installed on the stub



43
44
45
# File 'lib/cmdx/rspec/helpers.rb', line 43

def stub_task_skip(command, reason: nil, cause: nil, metadata: {}, **context)
  build_stub(command, :execute, CMDx::Signal.skipped(reason, metadata:, cause:), context)
end

#stub_task_skip!(command, reason: nil, cause: nil, metadata: {}, **context) ⇒ CMDx::Result

Stubs ‘command.execute!` (bang variant) to return a frozen skipped Result.

Parameters:

  • command (Class)

    the Task class to stub

  • reason (String, nil) (defaults to: nil)

    human-readable skip reason

  • cause (Exception, nil) (defaults to: nil)

    originating cause attached to the signal

  • metadata (Hash) (defaults to: {})

    payload exposed via ‘result.metadata`

  • context (Hash{Symbol => Object})

    context overrides forwarded to ‘command.new`

Returns:

  • (CMDx::Result)

    the frozen Result installed on the stub



55
56
57
# File 'lib/cmdx/rspec/helpers.rb', line 55

def stub_task_skip!(command, reason: nil, cause: nil, metadata: {}, **context)
  build_stub(command, :execute!, CMDx::Signal.skipped(reason, metadata:, cause:), context, strict: true)
end

#stub_task_success(command, metadata: {}, **context) ⇒ CMDx::Result

Stubs ‘command.execute` to return a frozen successful Result.

Examples:

stub_task_success(SomeTask, metadata: { id: 1 })

Parameters:

  • command (Class)

    the Task class to stub

  • metadata (Hash) (defaults to: {})

    payload exposed via ‘result.metadata`

  • context (Hash{Symbol => Object})

    context overrides forwarded to ‘command.new`

Returns:

  • (CMDx::Result)

    the frozen Result installed on the stub



21
22
23
# File 'lib/cmdx/rspec/helpers.rb', line 21

def stub_task_success(command, metadata: {}, **context)
  build_stub(command, :execute, CMDx::Signal.success(nil, metadata:), context)
end

#stub_task_success!(command, metadata: {}, **context) ⇒ CMDx::Result

Stubs ‘command.execute!` (bang variant) to return a frozen successful Result.

Parameters:

  • command (Class)

    the Task class to stub

  • metadata (Hash) (defaults to: {})

    payload exposed via ‘result.metadata`

  • context (Hash{Symbol => Object})

    context overrides forwarded to ‘command.new`

Returns:

  • (CMDx::Result)

    the frozen Result installed on the stub



31
32
33
# File 'lib/cmdx/rspec/helpers.rb', line 31

def stub_task_success!(command, metadata: {}, **context)
  build_stub(command, :execute!, CMDx::Signal.success(nil, metadata:), context, strict: true)
end

#stub_task_throw(command, upstream_result, metadata: {}, **context) ⇒ CMDx::Result

Stubs ‘command.execute` to return a frozen failed Result that echoes upstream_result. Models the `throw!`-then-propagate path used by nested tasks/workflows.

Parameters:

  • command (Class)

    the Task class to stub

  • upstream_result (CMDx::Result)

    the originating failure

  • metadata (Hash) (defaults to: {})
  • context (Hash{Symbol => Object})

Returns:

  • (CMDx::Result)

    the frozen Result installed on the stub



112
113
114
115
116
117
118
119
# File 'lib/cmdx/rspec/helpers.rb', line 112

def stub_task_throw(command, upstream_result, metadata: {}, **context)
  unless upstream_result.is_a?(CMDx::Result) && upstream_result.failed?
    raise ArgumentError,
          "upstream_result must be a failed CMDx::Result"
  end

  build_stub(command, :execute, CMDx::Signal.echoed(upstream_result, metadata:), context)
end

#stub_workflow_tasks(command) {|task| ... } ⇒ void

This method returns an undefined value.

Yields each distinct Task class reachable from a Workflow’s pipeline, in first-seen order, so callers can stub them in a single block.

Examples:

stub_workflow_tasks(MyWorkflow) { |t| stub_task_success(t) }

Parameters:

  • command (Class)

    a Workflow class (must include ‘CMDx::Workflow`)

Yield Parameters:

  • task (Class)

    a Task class referenced by the workflow pipeline

Raises:

  • (ArgumentError)

    when no block is given or ‘command` is not a Workflow



309
310
311
312
313
314
315
316
317
# File 'lib/cmdx/rspec/helpers.rb', line 309

def stub_workflow_tasks(command, &)
  if !block_given?
    raise ArgumentError, "block required"
  elsif !command.include?(Workflow)
    raise ArgumentError, "#{command.inspect} must be a workflow"
  end

  command.pipeline.flat_map(&:tasks).uniq.each(&)
end

#subscribe_telemetry(command, *events) { ... } ⇒ Array<CMDx::Telemetry::Event>

Subscribes to telemetry events on command‘s telemetry registry for the duration of the block. Captures every emitted event. Tasks subclassing command also fire (telemetry is cloned at class definition; the registry array is shared by reference until dup).

Examples:

events = subscribe_telemetry(MyCommand, :task_executed) { MyCommand.execute }
expect(events.map(&:name)).to eq([:task_executed])

Parameters:

  • command (Class)

    the Task class whose telemetry to listen on

  • events (Array<Symbol>)

    event names to subscribe to; defaults to all of Telemetry::EVENTS

Yields:

  • runs the block with subscribers attached

Returns:

  • (Array<CMDx::Telemetry::Event>)

    captured events in emission order

Raises:

  • (ArgumentError)


167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
# File 'lib/cmdx/rspec/helpers.rb', line 167

def subscribe_telemetry(command, *events, &)
  raise ArgumentError, "block required" unless block_given?

  events    = CMDx::Telemetry::EVENTS if events.empty?
  captured  = []
  telemetry = command.telemetry
  listener  = ->(event) { captured << event }

  events.each { |e| telemetry.subscribe(e, listener) }
  begin
    yield
  ensure
    events.each { |e| telemetry.unsubscribe(e, listener) }
  end

  captured
end

#unstub_task(command, **context) ⇒ void

This method returns an undefined value.

Restores ‘command.execute` to its original implementation. When `context` is supplied, only the matching argument signature is unstubbed.

Parameters:

  • command (Class)

    the Task class to unstub

  • context (Hash{Symbol => Object})

    argument signature whose stub to release



222
223
224
225
226
227
228
# File 'lib/cmdx/rspec/helpers.rb', line 222

def unstub_task(command, **context)
  if context.empty?
    allow(command).to receive(:execute).and_call_original
  else
    allow(command).to receive(:execute).with(**context).and_call_original
  end
end

#unstub_task!(command, **context) ⇒ void

This method returns an undefined value.

Restores ‘command.execute!` to its original implementation. When `context` is supplied, only the matching argument signature is unstubbed.

Parameters:

  • command (Class)

    the Task class to unstub

  • context (Hash{Symbol => Object})

    argument signature whose stub to release



236
237
238
239
240
241
242
# File 'lib/cmdx/rspec/helpers.rb', line 236

def unstub_task!(command, **context)
  if context.empty?
    allow(command).to receive(:execute!).and_call_original
  else
    allow(command).to receive(:execute!).with(**context).and_call_original
  end
end

#with_cmdx_chain(command) { ... } ⇒ CMDx::Chain?

Captures the Chain produced by command‘s execution within the block. Subscribes to command’s ‘:task_executed` telemetry to grab the chain reference before Runtime teardown clears it.

Examples:

chain = with_cmdx_chain(MyWorkflow) { MyWorkflow.execute }
expect(chain.size).to be > 1

Parameters:

  • command (Class)

    the Task class whose chain to capture

Yields:

  • the block to execute

Returns:

  • (CMDx::Chain, nil)

    the chain (frozen by Runtime), or nil when command didn’t run as a root during the block

Raises:

  • (ArgumentError)


197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/cmdx/rspec/helpers.rb', line 197

def with_cmdx_chain(command)
  raise ArgumentError, "block required" unless block_given?

  captured  = nil
  telemetry = command.telemetry
  listener  = lambda do |event|
    captured ||= event.payload[:result].chain if event.root
  end

  telemetry.subscribe(:task_executed, listener)
  begin
    yield
  ensure
    telemetry.unsubscribe(:task_executed, listener)
  end

  captured
end