ruby_llm-test 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6320355012e11036b37b8a13d6a7bd5782e573a1e7ab1a94601d67836e3e02ab
4
- data.tar.gz: 566e774f1e9ac7fb636b5f05eeef52fc27565eb2d9fb9997f0f14b6630ba67c0
3
+ metadata.gz: 8a0eae1a8d1ab14addc132333aa7f6baef9695e8a13c0160ee7ff85b5114c2a9
4
+ data.tar.gz: a45059e4ae9b0f182f791b43df7905b25a11d38d7a3d87878d2e6f0ad8a4421f
5
5
  SHA512:
6
- metadata.gz: 4a5b97108962cfa24230b4733e4357f2042ed6240dbe934d466567c102aadfee07224d0762b0105121c4092879d8ae5ef8f98ef7e76146170e366057aa09f15d
7
- data.tar.gz: e30334a2adf81c0faeb482daf76a96dda06efa7664f567c2778a6aafd1e02c934686f33f9302f6533eae184f390c7f7bba7355e5b0fd09dc2408ff79276282eb
6
+ metadata.gz: 4504b45b39bfea4799804b39f14f54560650646269eded1b0f3a50f019cf803f09a9bc04a82c7b4a66ddf88e345843c8be4bd4396655c4e7d11051bcafda200b
7
+ data.tar.gz: 7583a4bc1e8b89d0c2d358eb3d90956205ca3d622d06907045e2e48827c3cd7b9444433876e84af94b81d60ea1e416c872f2d81e2c7aaef063ce1d61d57f1af5
data/README.md CHANGED
@@ -1,10 +1,19 @@
1
1
  # RubyLLM::Test
2
2
 
3
- This gem provides testing utilities for RubyLLM, a Ruby library for working with large language models (LLMs). It enables calls to LLM's to be stubbed so that the surrounding application logic can be tested without making actual calls to the LLM. This is particularly useful for testing code that interacts with LLMs, as it allows developers to simulate responses from the LLM without incurring the cost, latency, or randomness of real API calls.
3
+ This gem provides testing utilities for RubyLLM, a Ruby library for working with large language models (LLMs). It enables calls to LLMs to be stubbed so that the surrounding application logic can be tested without making actual calls to the LLM. This is particularly useful for testing code that interacts with LLMs, as it allows developers to simulate responses from the LLM without incurring the cost, latency, or randomness of real API calls.
4
+
5
+ ```ruby
6
+ RubyLLM::Test.stub_response("Outlook good")
7
+
8
+ chat = RubyLLM.chat
9
+ response = chat.ask "What are the odds this works?"
10
+
11
+ assert_equal "Outlook good", response.content
12
+ ```
4
13
 
5
14
  ## Installation
6
15
 
7
- Add this line to your application's Gemfile:
16
+ Add this line to your application's Gemfile in the test group:
8
17
 
9
18
  ```ruby
10
19
  gem 'ruby_llm-test'
@@ -100,6 +109,23 @@ RubyLLM::Test.with_responses('Hello, world!') do
100
109
  end
101
110
  ```
102
111
 
112
+ ### Testing Arguments
113
+
114
+ You can verify arguments passed to the LLM by checking the requests received by the test provider with methods `requests` and `last_request`.
115
+
116
+ ```ruby
117
+ RubyLLM::Test.stub_response('Hello, world!')
118
+ chat = RubyLLM.chat(model: 'gpt-5-nano')
119
+ chat.with_tools(GreeterTool)
120
+ chat.ask('Hello?')
121
+ request = RubyLLM::Test.last_request
122
+
123
+ assert_equal 'gpt-5-nano', request.model
124
+ assert_includes request.tool_classes, GreeterTool
125
+ ```
126
+
127
+ Any parameters passed to the Provider's `complete` method are available on the request object. The `tool_classes` method is a helper that returns the classes of any tools included in the request.
128
+
103
129
  ## Development
104
130
 
105
131
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -2,27 +2,69 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Test
5
- # This class encapsulates all the parameters that are passed to the `complete` method of the `Test` class. It
6
- # serves as a structured way to manage and access these parameters throughout the testing process.
5
+ # This class encapsulates all the parameters that are passed to the `complete` method of the wrapped provider. It
6
+ # stores the raw arguments and uses the wrapped provider's actual method signature to expose named accessors for
7
+ # inspection in tests.
7
8
  class CompleteParameters
8
- attr_reader :messages, :tools, :temperature, :model, :params, :headers, :schema, :thinking, :tool_prefs, :block
9
-
10
- def initialize(messages:, tools:, temperature:, model:, params:, # rubocop:disable Metrics/ParameterLists
11
- headers:, schema:, thinking:, tool_prefs:, block:)
12
- @messages = messages
13
- @tools = tools
14
- @temperature = temperature
15
- @model = model
16
- @params = params
17
- @headers = headers
18
- @schema = schema
19
- @thinking = thinking
20
- @tool_prefs = tool_prefs
9
+ attr_reader :args, :kwargs, :block, :parameter_definitions
10
+
11
+ def self.capture_from(provider, *args, **kwargs, &block)
12
+ parameters = provider.method(:complete).parameters
13
+ new(args:, kwargs:, block:, parameter_definitions: parameters)
14
+ end
15
+
16
+ def initialize(args:, kwargs:, block:, parameter_definitions:)
17
+ @args = args
18
+ @kwargs = kwargs
21
19
  @block = block
20
+ @parameter_definitions = parameter_definitions
22
21
  end
23
22
 
24
23
  def block_received?
25
- !@block.nil?
24
+ !block.nil?
25
+ end
26
+
27
+ def [](name)
28
+ name = name.to_sym
29
+ return block if name == :block
30
+
31
+ positional_name_to_value[name] || kwargs[name]
32
+ end
33
+
34
+ def key?(name)
35
+ name = name.to_sym
36
+ name == :block || positional_name_to_value.key?(name) || kwargs.key?(name)
37
+ end
38
+
39
+ def to_h
40
+ positional_name_to_value.merge(kwargs).merge(block: block)
41
+ end
42
+
43
+ def method_missing(name, *call_args)
44
+ return super unless call_args.empty?
45
+ return self[name] if key?(name)
46
+
47
+ super
48
+ end
49
+
50
+ def respond_to_missing?(name, include_private = false)
51
+ key?(name) || super
52
+ end
53
+
54
+ def tool_classes
55
+ (kwargs[:tools] || {}).values.map(&:class)
56
+ end
57
+
58
+ private
59
+
60
+ def positional_name_to_value
61
+ @positional_name_to_value ||= begin
62
+ positional_names = parameter_definitions
63
+ .select { |parameter| %i[req opt].include?(parameter.first) }
64
+ .map(&:last)
65
+
66
+ positional_names.each_with_index.to_h { |param_name, index| [ param_name, args[index] ] }
67
+ end
26
68
  end
27
69
  end
28
70
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module Test
5
+ # This class serves as a test harness for stubbing responses and recording requests sent to the provider.
6
+ # It allows tests to set up expected responses and then verify that the provider received the correct
7
+ # parameters.
8
+ class Harness
9
+ attr_reader :requests
10
+
11
+ def initialize
12
+ reset
13
+ end
14
+
15
+ # Pass in a RubyLLM::Message to have full control; strings and hashes will be wrapped in a message
16
+ def stub_response(body)
17
+ responses << body
18
+ end
19
+
20
+ def stub_responses(*bodies)
21
+ responses.concat(bodies)
22
+ end
23
+
24
+ def with_responses(*bodies)
25
+ previous_responses = responses.dup
26
+ @responses = []
27
+ stub_responses(*bodies)
28
+ yield
29
+ ensure
30
+ @responses = previous_responses
31
+ end
32
+
33
+ def next_response
34
+ responses.shift
35
+ end
36
+
37
+ def responses_empty?
38
+ responses.empty?
39
+ end
40
+
41
+ def record_request(request)
42
+ requests << request
43
+ end
44
+
45
+ def last_request
46
+ requests.last
47
+ end
48
+
49
+ def clear_requests
50
+ requests.clear
51
+ end
52
+
53
+ def reset
54
+ @responses = []
55
+ @requests = []
56
+ end
57
+
58
+ private
59
+
60
+ def responses
61
+ @responses ||= []
62
+ end
63
+ end
64
+ end
65
+ end
@@ -12,7 +12,7 @@ module RubyLLM
12
12
  module ResolveWithTestProvider
13
13
  def resolve(...)
14
14
  model, provider_instance = super
15
- [ model, Test::TestProvider.new(provider_instance, RubyLLM::Test) ]
15
+ [ model, Test::TestProvider.new(provider_instance, RubyLLM::Test.send(:harness)) ]
16
16
  end
17
17
  end
18
18
  end
@@ -6,24 +6,15 @@ module RubyLLM
6
6
  # the `complete` method, allowing tests to assert that the correct parameters were passed and to simulate
7
7
  # responses from the provider.
8
8
  class TestProvider < SimpleDelegator
9
- extend Forwardable
10
-
11
- attr_reader :complete_calls
12
-
13
- def_delegators :last_call, :messages, :tools, :temperature, :model, :params, :headers, :schema, :thinking,
14
- :tool_prefs, :block_received?
15
-
16
- def initialize(provider, test_harness = RubyLLM::Test)
9
+ def initialize(provider, test_harness)
17
10
  super(provider)
18
11
  @test_harness = test_harness
19
- @complete_calls = []
20
12
  end
21
13
 
22
- def complete(messages, tools:, temperature:, model:, params: {}, # rubocop:disable Metrics/ParameterLists
23
- headers: {}, schema: nil, thinking: nil, tool_prefs: nil, &block)
24
- @complete_calls << CompleteParameters.new(messages:, tools:, temperature:, model:, params:, headers:, schema:,
25
- thinking:, tool_prefs:, block:)
26
- raise Errors::NoResponseProvidedError, messages if @test_harness.responses_empty?
14
+ def complete(...)
15
+ parameters = CompleteParameters.capture_from(__getobj__, ...)
16
+ @test_harness.record_request(parameters)
17
+ raise Errors::NoResponseProvidedError, parameters.messages if @test_harness.responses_empty?
27
18
 
28
19
  response = @test_harness.next_response
29
20
  return response if response.is_a?(Message)
@@ -33,10 +24,6 @@ module RubyLLM
33
24
  content: response.is_a?(Hash) ? response.to_json : response
34
25
  )
35
26
  end
36
-
37
- def last_call
38
- @complete_calls.last
39
- end
40
27
  end
41
28
  end
42
29
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  module Test
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
data/lib/ruby_llm/test.rb CHANGED
@@ -7,43 +7,115 @@ loader = Zeitwerk::Loader.for_gem_extension(RubyLLM)
7
7
  loader.setup
8
8
 
9
9
  module RubyLLM
10
- # The Test module provides a simple way to stub responses from an LLM for testing purposes. You can use it to set up
11
- # predetermined responses that your tests can rely on, allowing you to test your code's behavior without making
12
- # actual calls to an LLM.
10
+ # The Test module provides a simple way to stub responses from an LLM for testing purposes.
11
+ # You can use it to set up predetermined responses that your tests can rely on, allowing
12
+ # you to test your code's behavior without making actual calls to an LLM.
13
13
  module Test
14
14
  class << self
15
+ # Reset the test harness state.
16
+ #
17
+ # This clears all queued stubbed responses and all recorded requests.
18
+ #
19
+ # Use this at the start of a test, or whenever you want to ensure no state
20
+ # carries over from a previous example.
15
21
  def reset
16
- @responses = nil
22
+ harness.reset
17
23
  end
18
24
 
19
- # Pass in a RubyLLM::Message to have full control; strings and hashes will be wrapped in a message
25
+ # Queue a single stubbed response.
26
+ #
27
+ # Parameters:
28
+ #
29
+ # - `body`: The response to queue. Pass a `RubyLLM::Message` to control the
30
+ # message directly. Strings and hashes are wrapped in a message
31
+ # automatically.
32
+ #
33
+ # This is useful when your test only needs one response from the model.
34
+ #
35
+ # Example:
36
+ #
37
+ # RubyLLM::Test.stub_response("Hello from the test harness!")
38
+ #
39
+ # chat = RubyLLM.chat
40
+ # response = chat.ask("Say hello")
41
+ # response.content
42
+ # # => "Hello from the test harness!"
20
43
  def stub_response(body)
21
- responses << body
44
+ harness.stub_response(body)
22
45
  end
23
46
 
47
+ # Queue multiple stubbed responses.
48
+ #
49
+ # Parameters:
50
+ #
51
+ # - `*bodies`: One or more responses to queue.
52
+ #
53
+ # Responses are returned in the same order they are provided, making this
54
+ # useful for tests that perform multiple LLM calls in sequence.
55
+ #
56
+ # Example:
57
+ #
58
+ # RubyLLM::Test.stub_responses("First reply", "Second reply")
59
+ #
60
+ # chat = RubyLLM.chat
61
+ # first_response = chat.ask("First question")
62
+ # second_response = chat.ask("Second question")
63
+ #
64
+ # first_response.content
65
+ # # => "First reply"
66
+ # second_response.content
67
+ # # => "Second reply"
24
68
  def stub_responses(*bodies)
25
- responses.concat(bodies)
69
+ harness.stub_responses(*bodies)
26
70
  end
27
71
 
28
- def with_responses(*bodies)
29
- previous_responses = responses.dup
30
- @responses = []
31
- stub_responses(*bodies)
32
- yield
33
- ensure
34
- @responses = previous_responses
72
+ # Run a block with a temporary set of stubbed responses.
73
+ #
74
+ # Parameters:
75
+ #
76
+ # - `*bodies`: The responses to make available inside the block.
77
+ # - `&block`: The code to run while those responses are active.
78
+ #
79
+ # The provided responses are available only for the duration of the block.
80
+ # This is useful when you want to scope stubbed responses to a single part
81
+ # of a test without affecting later assertions.
82
+ #
83
+ # Example:
84
+ #
85
+ # RubyLLM::Test.with_responses("Scoped reply") do
86
+ # chat = RubyLLM.chat
87
+ # chat.ask("Question")
88
+ # end
89
+ def with_responses(*bodies, &)
90
+ harness.with_responses(*bodies, &)
35
91
  end
36
92
 
37
- def next_response
38
- responses.shift
93
+ # Return all recorded requests.
94
+ #
95
+ # This is useful for assertions about prompts, parameters, or other request
96
+ # details captured by the harness.
97
+ def requests
98
+ harness.requests
39
99
  end
40
100
 
41
- def responses_empty?
42
- responses.empty?
101
+ # Return the most recent request.
102
+ #
103
+ # Use this when you only care about the latest request made during a test.
104
+ def last_request
105
+ harness.last_request
43
106
  end
44
107
 
45
- def responses
46
- @responses ||= []
108
+ # Clear all recorded requests.
109
+ #
110
+ # This leaves queued responses unchanged.
111
+ def clear_requests
112
+ harness.clear_requests
113
+ end
114
+
115
+ private
116
+
117
+ def harness
118
+ @harness ||= Harness.new
47
119
  end
48
120
  end
49
121
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-test
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Todd Kummer
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-04-22 00:00:00.000000000 Z
10
+ date: 2026-04-25 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ruby_llm
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 1.14.0
18
+ version: 1.5.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 1.14.0
25
+ version: 1.5.0
26
26
  description: Provides a RubyLLM::Provider that allows you to stub responses for testing
27
27
  purposes. You can stub individual responses or a sequence of responses, and you
28
28
  can also temporarily stub responses within a block.
@@ -37,13 +37,16 @@ files:
37
37
  - lib/ruby_llm/test.rb
38
38
  - lib/ruby_llm/test/complete_parameters.rb
39
39
  - lib/ruby_llm/test/errors/no_response_provided_error.rb
40
+ - lib/ruby_llm/test/harness.rb
40
41
  - lib/ruby_llm/test/resolve_with_test_provider.rb
41
42
  - lib/ruby_llm/test/test_provider.rb
42
43
  - lib/ruby_llm/test/version.rb
43
44
  homepage: https://github.com/RockSolt/ruby_llm-test
44
45
  licenses:
45
46
  - MIT
46
- metadata: {}
47
+ metadata:
48
+ rubygems_mfa_required: 'true'
49
+ homepage_uri: https://github.com/RockSolt/ruby_llm-test
47
50
  rdoc_options: []
48
51
  require_paths:
49
52
  - lib