agentic examples

Honest Doubles

Honest Doubles: every fake LLM in every agent test suite is lying a little - the question is whether anyone checks. The discipline: (1) don't mock what you don't own - wrap the vendor in an adapter whose interface YOU define; (2) verify every double against that interface (methods AND arity), so a rename breaks the test suite loudly instead of letting a thousand fakes drift into fiction.

Testing & Verification Round 12 Justin Searls exit 0

source on github

bundle exec ruby examples/honest_doubles.rb

a real captured run

HONEST DOUBLES (verify the fake against the port, every time)

  honest double: verified against CompletionPort - method AND arity match
    triage under test: {:ticket=>"I was charged twice", :label=>"billing"}

  drifted double: REJECTED before any test ran -
    double DriftedDouble#complete has drifted: port takes [[:req, :prompt], [:keyreq, :max_tokens]], double takes [[:req, :prompt]]

  without the verifier, the drifted double PASSES every test you
  write with it - `complete` responds, strings come back, green
  everywhere - while the real adapter takes max_tokens: and would
  raise ArgumentError on the very first production call. that's
  the treachery of unverified fakes: they don't fail, they VOUCH.
  the two rules, cheap to follow: own the boundary (one port class
  names everything you use from the vendor - the census says the
  smaller that surface, the better), and verify every double
  against it in the double's own definition, so interface drift
  breaks the suite at load time, not the demo. your tests are
  only as honest as their most casual fake.

source

# frozen_string_literal: true

# Honest Doubles: every fake LLM in every agent test suite is lying a
# little - the question is whether anyone checks. The discipline:
# (1) don't mock what you don't own - wrap the vendor in an adapter
# whose interface YOU define; (2) verify every double against that
# interface (methods AND arity), so a rename breaks the test suite
# loudly instead of letting a thousand fakes drift into fiction.
#
#   bundle exec ruby examples/honest_doubles.rb
#
# Runs offline; one double is honest, one drifted. Guess which passes.

require class="s">"bundler/setup"
require class="s">"agentic"

Agentic.logger.level = class="y">:fatal

# --- the owned boundary ----------------------------------------------------------
# We do NOT stub Agentic::LlmClient (we don't own it; its interface
# can change under us at gem-update speed). We define OUR port:
class CompletionPort
  # The whole vendor surface we permit ourselves to use, in one place
  def complete(prompt, max_tokens:)
    raise NotImplementedError
  end
end

# The real adapter would wrap Agentic::LlmClient. For tests, doubles:
class HonestDouble < CompletionPort
  def initialize(scripted)
    @scripted = scripted
  end

  def complete(prompt, max_tokens:)
    @scripted.fetch(prompt[/\w+/])
  end
end

# This one was written against LAST QUARTER's port and nobody noticed
# the port grew a keyword since - classic double drift
class DriftedDouble
  def complete(prompt)
    class="s">"sure, whatever you say"
  end
end

# --- the verifier: doubles must match the port they claim to be -----------------
def verify_double!(double, port)
  port_methods = port.public_instance_methods(false)
  port_methods.each do |name|
    unless double.respond_to?(name)
      raise ArgumentError, class="s">"double #{double.class} is missing ##{name}"
    end

    expected = port.instance_method(name).parameters
    actual = double.method(name).parameters
    # Compare shapes: required/optional/keyword names must line up
    if expected.map { |kind, n| [kind, n] } != actual.map { |kind, n| [kind, n] }
      raise ArgumentError, class="s">"double #{double.class}##{name} has drifted: " \
        class="s">"port takes #{expected.inspect}, double takes #{actual.inspect}"
    end
  end
end

# --- a consumer under test -------------------------------------------------------
def triage(port, ticket)
  label = port.complete(class="s">"classify: #{ticket}", max_tokens: 5)
  {ticket: ticket, label: label}
end

puts class="s">"HONEST DOUBLES (verify the fake against the port, every time)"
puts

honest = HonestDouble.new(class="s">"classify" => class="s">"billing")
verify_double!(honest, CompletionPort)
puts class="s">"  honest double: verified against CompletionPort - method AND arity match"
result = triage(honest, class="s">"I was charged twice")
puts class="s">"    triage under test: #{result.inspect}"
puts

drifted = DriftedDouble.new
begin
  verify_double!(drifted, CompletionPort)
  puts class="s">"  drifted double: verified?! the verifier has no teeth"
  exit(1)
rescue ArgumentError => e
  puts class="s">"  drifted double: REJECTED before any test ran -"
  puts class="s">"    #{e.message}"
end
puts
puts class="s">"  without the verifier, the drifted double PASSES every test you"
puts class="s">"  write with it - `complete` responds, strings come back, green"
puts class="s">"  everywhere - while the real adapter takes max_tokens: and would"
puts class="s">"  raise ArgumentError on the very first production call. that's"
puts class="s">"  the treachery of unverified fakes: they don't fail, they VOUCH."
puts class="s">"  the two rules, cheap to follow: own the boundary (one port class"
puts class="s">"  names everything you use from the vendor - the census says the"
puts class="s">"  smaller that surface, the better), and verify every double"
puts class="s">"  against it in the double's own definition, so interface drift"
puts class="s">"  breaks the suite at load time, not the demo. your tests are"
puts class="s">"  only as honest as their most casual fake."