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 +4 -4
- data/README.md +28 -2
- data/lib/ruby_llm/test/complete_parameters.rb +58 -16
- data/lib/ruby_llm/test/harness.rb +65 -0
- data/lib/ruby_llm/test/resolve_with_test_provider.rb +1 -1
- data/lib/ruby_llm/test/test_provider.rb +5 -18
- data/lib/ruby_llm/test/version.rb +1 -1
- data/lib/ruby_llm/test.rb +92 -20
- metadata +8 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8a0eae1a8d1ab14addc132333aa7f6baef9695e8a13c0160ee7ff85b5114c2a9
|
|
4
|
+
data.tar.gz: a45059e4ae9b0f182f791b43df7905b25a11d38d7a3d87878d2e6f0ad8a4421f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
6
|
-
#
|
|
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 :
|
|
9
|
-
|
|
10
|
-
def
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@
|
|
17
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
23
|
-
|
|
24
|
-
@
|
|
25
|
-
|
|
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
|
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.
|
|
11
|
-
#
|
|
12
|
-
#
|
|
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
|
-
|
|
22
|
+
harness.reset
|
|
17
23
|
end
|
|
18
24
|
|
|
19
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
69
|
+
harness.stub_responses(*bodies)
|
|
26
70
|
end
|
|
27
71
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
46
|
-
|
|
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.
|
|
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-
|
|
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.
|
|
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.
|
|
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
|