Module: Pikuri::Testing
- Defined in:
- lib/pikuri/testing.rb
Overview
Test support: run the real Agent (real run_loop, real
listeners, real tools/extensions, real Event stream, real ruby_llm loop)
against a scripted HTTP backend — no server, no network, no mock agent.
Not loaded by default: require 'pikuri/testing' from a spec. It lives off
the production load path (the core Zeitwerk loader ignores it).
How it works
Testing.fake_transport returns an ordinary Agent::ChatTransport whose
faraday_adapter is an anonymous Faraday::Adapter serving the responses
you scripted. Because Agent#build_context installs that adapter on the
agent's per-chat RubyLLM::Context (never the global RubyLLM.config),
ruby_llm's own loop runs for real — it parses tool calls, invokes your real
registered tools, appends observations, and re-requests — and each scripted
entry is the model's next turn. No hand-rolled agent loop, nothing to drift
against a ruby_llm upgrade.
require 'pikuri/testing'
transport = Pikuri::Testing.fake_transport do |llm|
llm.tool_call('calculator', expression: '2 + 2') # turn 1: call a real tool
llm.reply('The answer is 4.') # turn 2: final answer
end
agent = Pikuri::Agent.new(transport:, system_prompt: 'test') do |c|
c.add_listener capture
c.add_tool Pikuri::Tool::CALCULATOR
end
agent.run_loop(user_message: 'What is 2 + 2?')
# the real loop ran the real calculator; capture saw ToolCall/ToolResult/Assistant
Limitations
A fake transport feeds one ordered response list. Propagation to
sub-agents / the synthesizer works (they derive their transport via
parent.transport.with(...), and Data#with preserves the adapter), but
they share that one list — fine for a single linear script, not for
targeting a specific sub-agent's replies. Per-sub-agent scripting is a
future upgrade (dispatch by model id / distinct fake api_base).
Tool-call streaming (fragmented tool_calls deltas) is not modelled;
Script#stream streams text only.
Defined Under Namespace
Class Method Summary collapse
-
.build_adapter(responses, gate = nil) ⇒ Class
An anonymous
Faraday::Adaptersubclass that servesresponsesin order, one per HTTP request. -
.fake_transport(model: 'fake-model', context_window: 8192, api_base: 'http://fake.invalid/v1', gate: nil) {|script| ... } ⇒ Agent::ChatTransport
Build a fake transport from a scripted conversation.
Class Method Details
.build_adapter(responses, gate = nil) ⇒ Class
An anonymous Faraday::Adapter subclass that serves responses in order,
one per HTTP request. Closes over the (mutable) list, so the queue drains
across the several requests of one conversation regardless of how Faraday
reuses adapter instances.
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/pikuri/testing.rb', line 100 def self.build_adapter(responses, gate = nil) queue = responses # captured by reference; #call shifts from it Class.new(Faraday::Adapter) do # +define_method+ (not +def+) so the body closes over +queue+/+gate+; # +super+ must be explicit here (implicit super is illegal in a # define_method). define_method(:call) do |env| super(env) # base builds env.response; skipping it → "finish for nil" gate&.await # park here while the test holds the turn open entry = queue.shift or raise "Pikuri::Testing: no scripted response left for #{env.url}" if env.request.stream_response? # ruby_llm set req.options.on_data # v2 on_data routes to the chunk handler only at status 200; set it # before feeding, mirroring how a real adapter reads the status line # ahead of the body. env.status = 200 on_data = env.request.on_data size = 0 entry.fetch(:sse).each do |chunk| size += chunk.bytesize on_data.call(chunk, size, env) end save_response(env, 200, '', { 'Content-Type' => 'text/event-stream' }, nil) else save_response(env, 200, JSON.generate(entry.fetch(:json)), { 'Content-Type' => 'application/json' }, nil) end @app.call(env) end end end |
.fake_transport(model: 'fake-model', context_window: 8192, api_base: 'http://fake.invalid/v1', gate: nil) {|script| ... } ⇒ Agent::ChatTransport
Build a fake transport from a scripted conversation. Pass a block that calls Script verbs in the order the model should "reply"; each verb queues one HTTP response.
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/pikuri/testing.rb', line 74 def self.fake_transport(model: 'fake-model', context_window: 8192, api_base: 'http://fake.invalid/v1', gate: nil) script = Script.new yield script if block_given? Agent::ChatTransport.new( model: model, provider: :openai, assume_model_exists: true, # An api_base forces the connection-override path (Context#chat), the # only branch that honours faraday_adapter; the host is never dialed. api_base: api_base, api_key: 'not-needed', context_window: context_window, faraday_adapter: build_adapter(script.responses, gate) ) end |