Module: Spurline::Testing

Defined in:
lib/spurline/testing.rb

Overview

Test helpers for Spurline agents. Require in your spec_helper:

require "spurline/testing"

Then include in your specs:

include Spurline::Testing

Or let the auto-configuration handle it (included globally if RSpec is loaded).

Constant Summary collapse

TOOL_SOURCE_PATTERN =
/source=["']tool:(?<tool_name>[^"']+)["']/.freeze

Instance Method Summary collapse

Instance Method Details

#assert_tool_called(tool_name, with: {}, agent: nil, adapter: nil, audit_log: nil, session: nil) ⇒ true

Asserts that a tool was called, optionally with matching arguments.

Sources checked in order:

1) audit_log tool_call events
2) session turn tool_calls
3) StubAdapter call history (tool presence only)

Parameters:

Returns:

  • (true)


72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/spurline/testing.rb', line 72

def assert_tool_called(tool_name, with: {}, agent: nil, adapter: nil, audit_log: nil, session: nil)
  tool = tool_name.to_s
  expected_arguments = deep_symbolize(with || {})
  audit_entries, session_entries, adapter_instance = resolve_tool_call_sources(
    agent: agent,
    adapter: adapter,
    audit_log: audit_log,
    session: session
  )

  matched = match_tool_call(tool, expected_arguments, audit_entries, key: :tool) ||
            match_tool_call(tool, expected_arguments, session_entries, key: :name)
  return true if matched

  if tool_called_in_history?(tool, audit_entries, key: :tool) ||
     tool_called_in_history?(tool, session_entries, key: :name)
    raise_expectation!(
      "Expected tool '#{tool}' to be called#{format_expected_arguments(expected_arguments)}."
    )
  end

  if tool_called_in_adapter_history?(adapter_instance, tool)
    if expected_arguments.empty?
      return true
    end

    raise_expectation!(
      "Tool '#{tool}' was detected in StubAdapter call history, but argument assertions " \
      "require session or audit history. Pass `agent:`, `session:`, or `audit_log:`."
    )
  end

  raise_expectation!(
    "Expected tool '#{tool}' to be called#{format_expected_arguments(expected_arguments)}."
  )
end

#assert_trust_level(content, expected_trust) ⇒ true

Asserts that a Content object carries the expected trust level.

Parameters:

Returns:

  • (true)


129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/spurline/testing.rb', line 129

def assert_trust_level(content, expected_trust)
  unless content.is_a?(Spurline::Security::Content)
    raise_expectation!(
      "Expected Spurline::Security::Content, got #{content.class.name}."
    )
  end

  expected = expected_trust.to_sym
  actual = content.trust
  return true if actual == expected

  raise_expectation!("Expected trust level #{expected.inspect}, got #{actual.inspect}.")
end

#expect_no_injection { ... } ⇒ true

Asserts that no injection detection error is raised while evaluating the block.

Yields:

  • A call that runs through the context pipeline.

Returns:

  • (true)


113
114
115
116
117
118
119
120
121
122
# File 'lib/spurline/testing.rb', line 113

def expect_no_injection
  raise ArgumentError, "expect_no_injection requires a block" unless block_given?

  yield
  true
rescue *injection_error_classes => e
  raise_expectation!(
    "Expected no injection detection errors, but #{e.class.name} was raised: #{e.message}"
  )
end

#stub_text(text, turn: 1) ⇒ Object

Creates a stub text response that streams as chunks.



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/spurline/testing.rb', line 17

def stub_text(text, turn: 1)
  chunks = text.chars.each_slice(5).map do |chars|
    Spurline::Streaming::Chunk.new(
      type: :text,
      text: chars.join,
      turn: turn
    )
  end

  chunks << Spurline::Streaming::Chunk.new(
    type: :done,
    turn: turn,
    metadata: { stop_reason: "end_turn" }
  )

  { type: :text, text: text, chunks: chunks }
end

#stub_tool_call(tool_name, turn: 1, **arguments) ⇒ Object

Creates a stub tool call response.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/spurline/testing.rb', line 36

def stub_tool_call(tool_name, turn: 1, **arguments)
  tool_call_data = { name: tool_name.to_s, arguments: arguments }

  chunks = [
    Spurline::Streaming::Chunk.new(
      type: :tool_start,
      turn: turn,
      metadata: { tool_name: tool_name.to_s, arguments: arguments }
    ),
    Spurline::Streaming::Chunk.new(
      type: :done,
      turn: turn,
      metadata: {
        stop_reason: "tool_use",
        tool_call: tool_call_data,
      }
    ),
  ]

  { type: :tool_call, tool_call: tool_call_data, chunks: chunks }
end