cmdx-rspec 1.4.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/README.md +274 -173
  4. data/lib/cmdx/rspec/helpers.rb +244 -276
  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 -5
@@ -1,33 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Matcher to verify that a command is deprecated.
3
+ # Asserts a Task class (or instance) is marked deprecated via CMDx's
4
+ # `deprecation` declaration. Optionally constrains the deprecation
5
+ # behavior — pass it positionally, via `.with_behavior(:warn)`, or use
6
+ # the convenience chains `.with_warning`, `.with_logging`, `.with_error`.
4
7
  #
5
- # @param expected_behavior [Symbol, String, true, false, nil] Optional behavior to check
6
- # - `:warn` or `/warn/` - checks if deprecation includes warning
7
- # - `:log` or `/log/` - checks if deprecation includes logging
8
- # - `:raise` or `/raise/` or `true` - checks if deprecation raises or is truthy
9
- # - `:none` or `false` or `nil` - checks if deprecation is false or nil
10
- # - Any other value - checks for exact match
11
- #
12
- # @return [RSpec::Matchers::BuiltIn::BaseMatcher] The matcher instance
13
- #
14
- # @example Checking if a command is deprecated
15
- # expect(MyCommand).to be_deprecated
16
- #
17
- # @example Checking deprecated with raise behavior
18
- # expect(MyCommand).to be_deprecated(:raise)
19
- # expect(MyCommand).to be_deprecated.with_raise
20
- #
21
- # @example Checking deprecated with warning behavior
22
- # expect(MyCommand).to be_deprecated(:warn)
23
- # expect(MyCommand).to be_deprecated.with_warning
24
- #
25
- # @example Checking deprecated with logging behavior
26
- # expect(MyCommand).to be_deprecated(:log)
27
- # expect(MyCommand).to be_deprecated.with_logging
28
- #
29
- # @example Using chainable matchers
30
- # expect(MyCommand).to be_deprecated.with_behavior(:custom)
8
+ # @example
9
+ # expect(SomeTask).to be_deprecated.with_warning
31
10
  RSpec::Matchers.define :be_deprecated do |expected_behavior = nil|
32
11
  description do
33
12
  if (behavior = @expected_behavior || expected_behavior)
@@ -54,35 +33,18 @@ RSpec::Matchers.define :be_deprecated do |expected_behavior = nil|
54
33
  end
55
34
 
56
35
  match do |actual|
57
- # Handle both class and instance
58
- target = actual.is_a?(Class) ? actual : actual.class
36
+ target = actual.is_a?(Class) ? actual : actual.class
37
+ deprecation = target.respond_to?(:deprecation) ? target.deprecation : nil
38
+ next false unless deprecation
59
39
 
60
- # Check if deprecate setting exists and is truthy
61
- deprecate_setting = target.settings.deprecate
62
- return false unless deprecate_setting
40
+ behavior = @expected_behavior || expected_behavior
41
+ next true unless behavior
63
42
 
64
- # If no specific behavior expected, just check if deprecated
65
- behavior_to_check = @expected_behavior || expected_behavior
66
- return true unless behavior_to_check
67
-
68
- # Check specific behavior
69
- case behavior_to_check
70
- when :warn, /warn/
71
- deprecate_setting.to_s.include?("warn")
72
- when :log, /log/
73
- deprecate_setting.to_s.include?("log")
74
- when :raise, /raise/, true
75
- deprecate_setting == true || deprecate_setting.to_s.include?("raise")
76
- when :none, false, nil
77
- !deprecate_setting || deprecate_setting == false
78
- else
79
- deprecate_setting == behavior_to_check
80
- end
43
+ deprecation.instance_variable_get(:@value) == behavior
81
44
  end
82
45
 
83
- # Chainable matchers for specific behaviors
84
- chain(:with_raise) { @expected_behavior = :raise }
85
- chain(:with_logging) { @expected_behavior = :log }
86
46
  chain(:with_warning) { @expected_behavior = :warn }
47
+ chain(:with_logging) { @expected_behavior = :log }
48
+ chain(:with_error) { @expected_behavior = :error }
87
49
  chain(:with_behavior) { |behavior| @expected_behavior = behavior }
88
50
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify that a result was interrupted (skipped or failed).
4
+ #
5
+ # @example
6
+ # expect(MyCommand.execute).to be_interrupted
7
+ RSpec::Matchers.define :be_interrupted do
8
+ description { "have been interrupted" }
9
+
10
+ failure_message do |result|
11
+ "expected #{result.inspect} to have state #{CMDx::Signal::INTERRUPTED.inspect}, " \
12
+ "but was #{result.state.inspect}"
13
+ end
14
+
15
+ match do |result|
16
+ raise ArgumentError, "must be a CMDx::Result" unless result.is_a?(CMDx::Result)
17
+
18
+ result.state == CMDx::Signal::INTERRUPTED
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify that a result is "ko" (skipped or failed, anything but success).
4
+ #
5
+ # @example
6
+ # expect(MyCommand.execute).to be_ko
7
+ RSpec::Matchers.define :be_ko do
8
+ description { "have been ko" }
9
+
10
+ failure_message do |result|
11
+ "expected #{result.inspect} to be ko (skipped or failed), but status was #{result.status.inspect}"
12
+ end
13
+
14
+ match do |result|
15
+ raise ArgumentError, "must be a CMDx::Result" unless result.is_a?(CMDx::Result)
16
+
17
+ result.ko?
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify that a result is "ok" (success or skipped, anything but failed).
4
+ #
5
+ # @example
6
+ # expect(MyCommand.execute).to be_ok
7
+ RSpec::Matchers.define :be_ok do
8
+ description { "have been ok" }
9
+
10
+ failure_message do |result|
11
+ "expected #{result.inspect} to be ok (success or skipped), but status was #{result.status.inspect}"
12
+ end
13
+
14
+ match do |result|
15
+ raise ArgumentError, "must be a CMDx::Result" unless result.is_a?(CMDx::Result)
16
+
17
+ result.ok?
18
+ end
19
+ end
@@ -1,23 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Matcher to verify that a result represents a successful execution.
3
+ # Asserts a {CMDx::Result} represents a successful execution
4
+ # (`state: complete`, `status: success`). Additional keyword args are
5
+ # merged into a `result.to_h` inclusion check, so any Result field can be
6
+ # constrained inline (e.g. `be_successful(metadata: { id: 1 })`).
4
7
  #
5
- # @param data [Hash] Optional hash of additional attributes to match
6
- # @option data [Symbol] :state Expected state (defaults to CMDx::Result::COMPLETE)
7
- # @option data [Symbol] :status Expected status (defaults to CMDx::Result::SUCCESS)
8
- # @option data [Symbol] :outcome Expected outcome (defaults to CMDx::Result::SUCCESS)
9
- #
10
- # @return [RSpec::Matchers::BuiltIn::BaseMatcher] The matcher instance
11
- #
12
- # @raise [ArgumentError] if the actual value is not a CMDx::Result
13
- #
14
- # @example Checking if a result is successful
15
- # result = MyCommand.execute
16
- # expect(result).to be_successful
17
- #
18
- # @example Checking success with additional attributes
19
- # result = MyCommand.execute
20
- # expect(result).to be_successful(state: CMDx::Result::COMPLETE)
8
+ # @example
9
+ # expect(SomeTask.execute).to be_successful
21
10
  RSpec::Matchers.define :be_successful do |**data|
22
11
  description { "have been a success" }
23
12
 
@@ -25,9 +14,8 @@ RSpec::Matchers.define :be_successful do |**data|
25
14
  raise ArgumentError, "must be a CMDx::Result" unless result.is_a?(CMDx::Result)
26
15
 
27
16
  expect(result.to_h).to include(
28
- state: CMDx::Result::COMPLETE,
29
- status: CMDx::Result::SUCCESS,
30
- outcome: CMDx::Result::SUCCESS,
17
+ state: CMDx::Signal::COMPLETE,
18
+ status: CMDx::Signal::SUCCESS,
31
19
  **data
32
20
  )
33
21
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify that a result was retried at least once, optionally
4
+ # matching a specific retry count.
5
+ #
6
+ # @example Any retry
7
+ # expect(result).to have_been_retried
8
+ #
9
+ # @example Exact retry count
10
+ # expect(result).to have_been_retried(3)
11
+ RSpec::Matchers.define :have_been_retried do |expected_count = nil|
12
+ description do
13
+ expected_count ? "have been retried #{expected_count} times" : "have been retried"
14
+ end
15
+
16
+ failure_message do |result|
17
+ if expected_count
18
+ "expected #{result.inspect} to have been retried #{expected_count} times, but it was #{result.retries}"
19
+ else
20
+ "expected #{result.inspect} to have been retried, but it wasn't"
21
+ end
22
+ end
23
+
24
+ failure_message_when_negated do |result|
25
+ if expected_count
26
+ "expected #{result.inspect} not to have been retried #{expected_count} times, but it was"
27
+ else
28
+ "expected #{result.inspect} not to have been retried, but it was retried #{result.retries} times"
29
+ end
30
+ end
31
+
32
+ match do |result|
33
+ raise ArgumentError, "must be a CMDx::Result" unless result.is_a?(CMDx::Result)
34
+
35
+ if expected_count.nil?
36
+ result.retried?
37
+ else
38
+ result.retries == expected_count
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify that a failing task ran its rollback hook.
4
+ #
5
+ # @example
6
+ # expect(result).to have_been_rolled_back
7
+ RSpec::Matchers.define :have_been_rolled_back do
8
+ description { "have been rolled back" }
9
+
10
+ failure_message do |result|
11
+ "expected #{result.inspect} to have been rolled back, but it wasn't"
12
+ end
13
+
14
+ failure_message_when_negated do |result|
15
+ "expected #{result.inspect} not to have been rolled back, but it was"
16
+ end
17
+
18
+ match do |result|
19
+ raise ArgumentError, "must be a CMDx::Result" unless result.is_a?(CMDx::Result)
20
+
21
+ result.rolled_back?
22
+ end
23
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify that a task class registered a callback for +event+.
4
+ # Optional +callable+ further constrains the matcher; matched by `==`
5
+ # (Symbol method names, Proc/Object identity), or by class membership
6
+ # when given a class.
7
+ #
8
+ # @example Any callback for an event
9
+ # expect(MyCommand).to have_callback(:before_execution)
10
+ #
11
+ # @example A specific Symbol callback
12
+ # expect(MyCommand).to have_callback(:before_execution, :authenticate!)
13
+ #
14
+ # @example A callable instance class
15
+ # expect(MyCommand).to have_callback(:on_failed, AlertOnFailure)
16
+ RSpec::Matchers.define :have_callback do |event, callable = nil|
17
+ description do
18
+ callable.nil? ? "have callback for #{event.inspect}" : "have callback #{callable.inspect} for #{event.inspect}"
19
+ end
20
+
21
+ failure_message do |actual|
22
+ target = actual.is_a?(Class) ? actual : actual.class
23
+ entries = target.callbacks.registry[event] || []
24
+ if entries.empty?
25
+ "expected #{target} to register a callback for #{event.inspect}, but none were registered"
26
+ else
27
+ "expected #{target} to register #{callable.inspect} for #{event.inspect}, but had #{entries.map(&:first).inspect}"
28
+ end
29
+ end
30
+
31
+ match do |actual|
32
+ target = actual.is_a?(Class) ? actual : actual.class
33
+ next false unless target.respond_to?(:callbacks)
34
+
35
+ entries = target.callbacks.registry[event]
36
+ next false if entries.nil? || entries.empty?
37
+ next true if callable.nil?
38
+
39
+ entries.any? do |cb, _opts|
40
+ case callable
41
+ when Class then cb.is_a?(callable)
42
+ else cb == callable
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify that a {CMDx::Chain}'s root is a {CMDx::Result} for
4
+ # the given task class. Accepts either a Chain or a Result.
5
+ #
6
+ # @example
7
+ # expect(result).to have_chain_root(MyWorkflow)
8
+ RSpec::Matchers.define :have_chain_root do |task_class|
9
+ description { "have chain root #{task_class}" }
10
+
11
+ failure_message do |actual|
12
+ chain = extract_chain(actual)
13
+ "expected chain root to be #{task_class}, but was #{chain&.root&.task.inspect}"
14
+ end
15
+
16
+ match do |actual|
17
+ chain = extract_chain(actual)
18
+ next false if chain.nil?
19
+ next false if chain.root.nil?
20
+
21
+ chain.root.task <= task_class
22
+ end
23
+
24
+ define_method(:extract_chain) do |actual|
25
+ case actual
26
+ when CMDx::Chain then actual
27
+ when CMDx::Result then actual.chain
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify the size of a {CMDx::Chain}, accepting either a Chain
4
+ # directly or a {CMDx::Result} (whose +chain+ is read).
5
+ #
6
+ # @example
7
+ # expect(result).to have_chain_size(3)
8
+ RSpec::Matchers.define :have_chain_size do |expected|
9
+ description { "have chain size #{expected}" }
10
+
11
+ failure_message do |actual|
12
+ chain = extract_chain(actual)
13
+ "expected chain size to be #{expected}, but was #{chain&.size.inspect}"
14
+ end
15
+
16
+ match do |actual|
17
+ chain = extract_chain(actual)
18
+ next false if chain.nil?
19
+
20
+ chain.size == expected
21
+ end
22
+
23
+ define_method(:extract_chain) do |actual|
24
+ case actual
25
+ when CMDx::Chain then actual
26
+ when CMDx::Result then actual.chain
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify a {CMDx::Result}'s duration (in milliseconds) falls
4
+ # within an upper bound, lower bound, or both.
5
+ #
6
+ # @example
7
+ # expect(result).to have_duration(less_than: 100)
8
+ # expect(result).to have_duration(greater_than: 0.1)
9
+ # expect(result).to have_duration(greater_than: 1, less_than: 50)
10
+ RSpec::Matchers.define :have_duration do |less_than: nil, greater_than: nil|
11
+ description do
12
+ parts = []
13
+ parts << "greater than #{greater_than}ms" if greater_than
14
+ parts << "less than #{less_than}ms" if less_than
15
+ "have duration #{parts.join(' and ')}"
16
+ end
17
+
18
+ failure_message do |result|
19
+ "expected duration to satisfy bounds (less_than: #{less_than}, greater_than: #{greater_than}), but was #{result.duration}"
20
+ end
21
+
22
+ match do |result|
23
+ raise ArgumentError, "must be a CMDx::Result" unless result.is_a?(CMDx::Result)
24
+ raise ArgumentError, "provide :less_than and/or :greater_than" if less_than.nil? && greater_than.nil?
25
+ next false if result.duration.nil?
26
+
27
+ (less_than.nil? || result.duration < less_than) &&
28
+ (greater_than.nil? || result.duration > greater_than)
29
+ end
30
+ end
@@ -1,22 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Matcher to verify that a context is empty.
3
+ # Asserts the subject's context hash is empty. Accepts a `Hash`,
4
+ # {CMDx::Context}, or {CMDx::Result} (in which case `result.context` is
5
+ # inspected). Raises if the subject is none of those.
4
6
  #
5
- # @param context [Hash, CMDx::Context, CMDx::Result] The context to check
6
- # - If Hash, checks the hash directly
7
- # - If CMDx::Context, converts to hash and checks
8
- # - If CMDx::Result, extracts context and checks
9
- #
10
- # @return [RSpec::Matchers::BuiltIn::BaseMatcher] The matcher instance
11
- #
12
- # @raise [RuntimeError] if the context type is unknown
13
- #
14
- # @example Checking empty context from a hash
15
- # context = {}
16
- # expect(context).to have_empty_context
17
- #
18
- # @example Checking empty context from a result
19
- # result = MyCommand.execute
7
+ # @example
20
8
  # expect(result).to have_empty_context
21
9
  RSpec::Matchers.define :have_empty_context do
22
10
  description { "have an empty context" }
@@ -1,15 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Matcher to verify that a result has empty metadata.
3
+ # Asserts a {CMDx::Result}'s `metadata` hash is empty. Raises
4
+ # `ArgumentError` when the subject is not a Result.
4
5
  #
5
- # @param result [CMDx::Result] The result to check
6
- #
7
- # @return [RSpec::Matchers::BuiltIn::BaseMatcher] The matcher instance
8
- #
9
- # @raise [ArgumentError] if the actual value is not a CMDx::Result
10
- #
11
- # @example Checking if a result has empty metadata
12
- # result = MyCommand.execute
6
+ # @example
13
7
  # expect(result).to have_empty_metadata
14
8
  RSpec::Matchers.define :have_empty_metadata do
15
9
  description { "have an empty metadata" }
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify that a {CMDx::Result}, task instance, or {CMDx::Errors}
4
+ # carries at least one error under +key+. Optional +messages+ further
5
+ # constrain the matcher to specific error strings.
6
+ #
7
+ # @example Any error on :email
8
+ # expect(result).to have_errors_on(:email)
9
+ #
10
+ # @example Specific message
11
+ # expect(result).to have_errors_on(:email, "is required")
12
+ #
13
+ # @example Multiple messages (all must be present)
14
+ # expect(task).to have_errors_on(:email, "is required", "is invalid")
15
+ RSpec::Matchers.define :have_errors_on do |key, *messages|
16
+ description do
17
+ if messages.empty?
18
+ "have errors on #{key.inspect}"
19
+ else
20
+ "have errors on #{key.inspect} matching #{messages.inspect}"
21
+ end
22
+ end
23
+
24
+ failure_message do |actual|
25
+ errors = extract_errors(actual)
26
+ if errors.nil?
27
+ "expected #{actual.inspect} to expose an Errors collection"
28
+ elsif !errors.key?(key)
29
+ "expected errors on #{key.inspect}, but only had: #{errors.keys.inspect}"
30
+ else
31
+ "expected errors on #{key.inspect} to include #{messages.inspect}, but had #{errors[key].inspect}"
32
+ end
33
+ end
34
+
35
+ match do |actual|
36
+ errors = extract_errors(actual)
37
+ next false if errors.nil?
38
+ next false unless errors.key?(key)
39
+
40
+ messages.all? { |m| errors.added?(key, m) }
41
+ end
42
+
43
+ define_method(:extract_errors) do |actual|
44
+ case actual
45
+ when CMDx::Errors then actual
46
+ when CMDx::Result, CMDx::Task then actual.errors
47
+ else actual.respond_to?(:errors) ? actual.errors : nil
48
+ end
49
+ end
50
+ end
@@ -1,25 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Matcher to verify that a result represents a failure.
3
+ # Asserts a {CMDx::Result} failed (`state: interrupted`,
4
+ # `status: failed`). Extra keyword args constrain other `result.to_h`
5
+ # fields such as `:reason`, `:cause`, or `:metadata`.
4
6
  #
5
- # @param data [Hash] Optional hash of additional attributes to match
6
- # @option data [Symbol] :state Expected state (defaults to CMDx::Result::INTERRUPTED)
7
- # @option data [Symbol] :status Expected status (defaults to CMDx::Result::FAILED)
8
- # @option data [Symbol] :outcome Expected outcome (defaults to CMDx::Result::FAILED)
9
- # @option data [String] :reason Expected reason string
10
- # @option data [CMDx::FailFault] :cause Expected cause fault
11
- #
12
- # @return [RSpec::Matchers::BuiltIn::BaseMatcher] The matcher instance
13
- #
14
- # @raise [ArgumentError] if the actual value is not a CMDx::Result
15
- #
16
- # @example Checking if a result is a failure
17
- # result = MyCommand.execute
18
- # expect(result).to have_failed
19
- #
20
- # @example Checking failure with specific reason
21
- # result = MyCommand.execute
22
- # expect(result).to have_failed(reason: "Custom error message")
7
+ # @example
8
+ # expect(result).to have_failed(cause: be_a(NoMethodError))
23
9
  RSpec::Matchers.define :have_failed do |**data|
24
10
  description { "have been a failure" }
25
11
 
@@ -27,11 +13,8 @@ RSpec::Matchers.define :have_failed do |**data|
27
13
  raise ArgumentError, "must be a CMDx::Result" unless result.is_a?(CMDx::Result)
28
14
 
29
15
  expect(result.to_h).to include(
30
- state: CMDx::Result::INTERRUPTED,
31
- status: CMDx::Result::FAILED,
32
- outcome: CMDx::Result::FAILED,
33
- reason: CMDx::Locale.t("cmdx.faults.unspecified"),
34
- cause: be_a(CMDx::FailFault),
16
+ state: CMDx::Signal::INTERRUPTED,
17
+ status: CMDx::Signal::FAILED,
35
18
  **data
36
19
  )
37
20
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify that a task class declares an input named +name+.
4
+ # Optional keyword arguments are matched against the input's serialized
5
+ # {CMDx::Input#to_h} (partial match — only provided keys are checked).
6
+ #
7
+ # @example Existence check
8
+ # expect(MyCommand).to have_input(:user_id)
9
+ #
10
+ # @example Required input
11
+ # expect(MyCommand).to have_input(:user_id, required: true)
12
+ #
13
+ # @example Type / coercion check (matches against the +options+ hash)
14
+ # expect(MyCommand).to have_input(:user_id, options: hash_including(coerce: :integer))
15
+ RSpec::Matchers.define :have_input do |name, **expected|
16
+ description do
17
+ expected.empty? ? "have input #{name.inspect}" : "have input #{name.inspect} matching #{expected.inspect}"
18
+ end
19
+
20
+ failure_message do |actual|
21
+ target = actual.is_a?(Class) ? actual : actual.class
22
+ input = target.inputs.registry[name.to_sym]
23
+ if input.nil?
24
+ "expected #{target} to declare input #{name.inspect}, but registry has #{target.inputs.registry.keys.inspect}"
25
+ else
26
+ "expected #{target}'s input #{name.inspect} to match #{expected.inspect}, but it was #{input.to_h.inspect}"
27
+ end
28
+ end
29
+
30
+ match do |actual|
31
+ target = actual.is_a?(Class) ? actual : actual.class
32
+ next false unless target.respond_to?(:inputs)
33
+
34
+ input = target.inputs.registry[name.to_sym]
35
+ next false if input.nil?
36
+ next true if expected.empty?
37
+
38
+ schema = input.to_h
39
+ expected.all? { |k, v| values_match?(v, schema[k]) }
40
+ end
41
+ end
@@ -1,26 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Matcher to verify that a context matches the given data.
3
+ # Asserts the subject's context includes the supplied keys/values.
4
+ # Accepts a `Hash`, {CMDx::Context}, or {CMDx::Result}. With no keyword
5
+ # args, delegates to {have_empty_context}.
4
6
  #
5
- # @param data [Hash] Optional hash of key-value pairs to match in the context
6
- # If empty, delegates to {have_empty_context}
7
- #
8
- # @param context [Hash, CMDx::Context, CMDx::Result] The context to check
9
- # - If Hash, checks the hash directly
10
- # - If CMDx::Context, converts to hash and checks
11
- # - If CMDx::Result, extracts context and checks
12
- #
13
- # @return [RSpec::Matchers::BuiltIn::BaseMatcher] The matcher instance
14
- #
15
- # @raise [RuntimeError] if the context type is unknown
16
- #
17
- # @example Checking context matches specific values
18
- # result = MyCommand.execute(user_id: 123, role: "admin")
19
- # expect(result).to have_matching_context(user_id: 123, role: "admin")
20
- #
21
- # @example Checking empty context
22
- # result = MyCommand.execute
23
- # expect(result).to have_matching_context
7
+ # @example
8
+ # expect(result).to have_matching_context(stored_id: 123)
24
9
  RSpec::Matchers.define :have_matching_context do |**data|
25
10
  description { "have matching context" }
26
11
 
@@ -1,23 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Matcher to verify that a result's metadata matches the given data.
3
+ # Asserts a {CMDx::Result}'s `metadata` hash includes the supplied
4
+ # keys/values. With no keyword args, delegates to {have_empty_metadata}.
5
+ # Raises `ArgumentError` when the subject is not a Result.
4
6
  #
5
- # @param data [Hash] Optional hash of key-value pairs to match in the metadata
6
- # If empty, delegates to {have_empty_metadata}
7
- #
8
- # @param result [CMDx::Result] The result to check
9
- #
10
- # @return [RSpec::Matchers::BuiltIn::BaseMatcher] The matcher instance
11
- #
12
- # @raise [ArgumentError] if the actual value is not a CMDx::Result
13
- #
14
- # @example Checking metadata matches specific values
15
- # result = MyCommand.execute
16
- # expect(result).to have_matching_metadata(key: "value", count: 42)
17
- #
18
- # @example Checking empty metadata
19
- # result = MyCommand.execute
20
- # expect(result).to have_matching_metadata
7
+ # @example
8
+ # expect(result).to have_matching_metadata(status_code: 500)
21
9
  RSpec::Matchers.define :have_matching_metadata do |**data|
22
10
  description { "have matching metadata" }
23
11
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Matcher to verify that a task class registered +middleware+. When given
4
+ # a class, matches by `is_a?`; otherwise by `==` (works for module-level
5
+ # callables like `MyMiddleware` referenced by name).
6
+ #
7
+ # @example
8
+ # expect(MyCommand).to have_middleware(LoggingMiddleware)
9
+ RSpec::Matchers.define :have_middleware do |middleware|
10
+ description { "have middleware #{middleware.inspect}" }
11
+
12
+ failure_message do |actual|
13
+ target = actual.is_a?(Class) ? actual : actual.class
14
+ "expected #{target} to register middleware #{middleware.inspect}, but had #{target.middlewares.registry.inspect}"
15
+ end
16
+
17
+ match do |actual|
18
+ target = actual.is_a?(Class) ? actual : actual.class
19
+ next false unless target.respond_to?(:middlewares)
20
+
21
+ target.middlewares.registry.any? do |entry|
22
+ m, = entry
23
+ case middleware
24
+ when Class then m.is_a?(middleware) || m == middleware
25
+ else m == middleware
26
+ end
27
+ end
28
+ end
29
+ end