Module: RubyLLM::Contract::RSpec::Helpers

Defined in:
lib/ruby_llm/contract/rspec/helpers.rb

Instance Method Summary collapse

Instance Method Details

#stub_all_steps(response: nil, responses: nil, &block) ⇒ Object

Set a global test adapter for ALL steps.

stub_all_steps(response: { default: true })

Supports an optional block form — the previous adapter is restored after the block returns (even if it raises):

stub_all_steps(response: { default: true }) do
  # all steps use test adapter
end
# original adapter restored


101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/ruby_llm/contract/rspec/helpers.rb', line 101

def stub_all_steps(response: nil, responses: nil, &block)
  adapter = build_test_adapter(response: response, responses: responses)

  if block
    previous = RubyLLM::Contract.configuration.default_adapter
    begin
      RubyLLM::Contract.configuration.default_adapter = adapter
      yield
    ensure
      RubyLLM::Contract.configuration.default_adapter = previous
    end
  else
    RubyLLM::Contract.configure { |c| c.default_adapter = adapter }
  end
end

#stub_step(step_class, response: nil, responses: nil, &block) ⇒ Object

Stub a step to return a canned response without API calls.

stub_step(ClassifyTicket, response: { priority: "high" })
result = ClassifyTicket.run("test")
result.parsed_output  # => {priority: "high"}

Only affects the specified step — other steps are not affected.

With a block, the stub is scoped — cleaned up after the block:

stub_step(ClassifyTicket, response: data) do
  # only stubbed inside this block
end
# ClassifyTicket no longer stubbed

Without a block, the stub lives until the RSpec example ends.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/ruby_llm/contract/rspec/helpers.rb', line 24

def stub_step(step_class, response: nil, responses: nil, &block)
  adapter = build_test_adapter(response: response, responses: responses)

  if block
    # Block form: use thread-local overrides with save/restore for real scoping
    overrides = RubyLLM::Contract.step_adapter_overrides
    previous = overrides[step_class]
    overrides[step_class] = adapter
    begin
      yield
    ensure
      if previous
        overrides[step_class] = previous
      else
        overrides.delete(step_class)
      end
    end
  else
    # Non-block: use RSpec allow (auto-cleaned after example)
    allow(step_class).to receive(:run).and_wrap_original do |original, input, **kwargs|
      context = kwargs[:context] || {}
      unless context.key?(:adapter) || context.key?("adapter")
        context = context.merge(adapter: adapter)
      end
      original.call(input, context: context)
    end
  end
end

#stub_steps(stubs, &block) ⇒ Object

Stub multiple steps at once with different responses. Takes a hash of step_class => options. Requires a block.

stub_steps(
  ClassifyTicket => { response: { priority: "high" } },
  RouteToTeam => { response: { team: "billing" } }
) do
  result = TicketPipeline.run("test")
end

Raises:

  • (ArgumentError)


63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/ruby_llm/contract/rspec/helpers.rb', line 63

def stub_steps(stubs, &block)
  raise ArgumentError, "stub_steps requires a block" unless block

  overrides = RubyLLM::Contract.step_adapter_overrides
  previous = {}

  stubs.each do |step_class, opts|
    opts = opts.transform_keys(&:to_sym)
    adapter = build_test_adapter(**opts)
    previous[step_class] = overrides[step_class]
    overrides[step_class] = adapter
  end

  begin
    yield
  ensure
    stubs.each_key do |step_class|
      if previous[step_class]
        overrides[step_class] = previous[step_class]
      else
        overrides.delete(step_class)
      end
    end
  end
end