rspec-agents 0.1.0 → 0.1.1
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/bin/rspec-agents +24 -24
- data/lib/rspec/agents/backtrace_helper.rb +43 -0
- data/lib/rspec/agents/cli.rb +24 -13
- data/lib/rspec/agents/dsl/runner_factory.rb +6 -14
- data/lib/rspec/agents/dsl/scenario_set_dsl.rb +8 -0
- data/lib/rspec/agents/dsl/test_context.rb +13 -3
- data/lib/rspec/agents/dsl.rb +5 -4
- data/lib/rspec/agents/event_bus.rb +37 -4
- data/lib/rspec/agents/observers/base.rb +1 -1
- data/lib/rspec/agents/observers/parallel_terminal_observer.rb +1 -1
- data/lib/rspec/agents/parallel/ui/interactive_ui.rb +5 -0
- data/lib/rspec/agents/parallel/ui/interleaved_ui.rb +5 -0
- data/lib/rspec/agents/runners/headless_runner.rb +6 -30
- data/lib/rspec/agents/runners/parallel_terminal_runner.rb +28 -4
- data/lib/rspec/agents/runners/run_data_uploader.rb +71 -0
- data/lib/rspec/agents/runners/terminal_runner.rb +36 -15
- data/lib/rspec/agents/runners/user_simulator.rb +13 -16
- data/lib/rspec/agents/serialization/base_renderer.rb +100 -0
- data/lib/rspec/agents/serialization/conversation_renderer.rb +41 -38
- data/lib/rspec/agents/serialization/extension.rb +38 -13
- data/lib/rspec/agents/serialization/extensions/copy_example_json_extension.rb +53 -0
- data/lib/rspec/agents/serialization/extensions/copy_example_json_templates/_copy_example_json.js +31 -0
- data/lib/rspec/agents/serialization/extensions/core_extension.rb +18 -2
- data/lib/rspec/agents/serialization/ir.rb +403 -0
- data/lib/rspec/agents/serialization/run_data_builder.rb +7 -2
- data/lib/rspec/agents/serialization/templates/_conversation_fragment.html.haml +1 -1
- data/lib/rspec/agents/serialization/templates/_scripts.js +0 -25
- data/lib/rspec/agents/serialization/templates/_styles.css +125 -0
- data/lib/rspec/agents/serialization/templates/conversation_document.html.haml +6 -5
- data/lib/rspec/agents/serialization/test_suite_renderer.rb +9 -56
- data/lib/rspec/agents/serialization.rb +52 -19
- data/lib/rspec/agents/spec_executor.rb +11 -25
- data/lib/rspec/agents/turn_executor.rb +1 -0
- data/lib/rspec/agents/version.rb +1 -1
- data/lib/rspec/agents.rb +3 -1
- metadata +11 -6
- data/lib/rspec/agents/isolated_event_bus.rb +0 -86
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae027e89bf78dc8f780f04c71c237d97d986cd79943c9f975cecca638b778368
|
|
4
|
+
data.tar.gz: 3e4ce43b5c859fe56157b3aeadfdf4e1cb2d70570196a9cbd0175377e68d2c1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2edb7ff68a8037905032d261df6fa21889597ac83ac1edd8024241bed3d8b954db1ac16a280b58fb09c96b561470e78fcc72bf2d06b0f766765b5bbadbc140ff
|
|
7
|
+
data.tar.gz: b1dae96b52dcc29b198719f86d378aea0f9b8cc84e97de995ffc9aa99363e17948c865c509df00c56034b3389b3dbb946974d77ae6627f0ba646180f71280a50
|
data/bin/rspec-agents
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# Unified CLI for rspec-agents
|
|
5
|
-
#
|
|
6
|
-
# Usage:
|
|
7
|
-
# rspec-agents [run] [options] [paths...] # Single-process (default)
|
|
8
|
-
# rspec-agents parallel [options] [paths...] # Parallel with workers
|
|
9
|
-
# rspec-agents render <json_file> [options] # Render HTML from JSON
|
|
10
|
-
# rspec-agents worker # Internal: worker mode
|
|
11
|
-
|
|
12
|
-
# macOS fork safety - must be set before any Objective-C code loads
|
|
13
|
-
ENV["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
|
|
14
|
-
|
|
15
|
-
# Sync output streams for worker mode
|
|
16
|
-
$stdout.sync = true
|
|
17
|
-
$stderr.sync = true
|
|
18
|
-
|
|
19
|
-
require "rspec/agents"
|
|
20
|
-
require "rspec/agents/cli"
|
|
21
|
-
|
|
22
|
-
# Run CLI
|
|
23
|
-
exit_code = RSpec::Agents::CLI.run(ARGV)
|
|
24
|
-
exit(exit_code || 0)
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Unified CLI for rspec-agents
|
|
5
|
+
#
|
|
6
|
+
# Usage:
|
|
7
|
+
# rspec-agents [run] [options] [paths...] # Single-process (default)
|
|
8
|
+
# rspec-agents parallel [options] [paths...] # Parallel with workers
|
|
9
|
+
# rspec-agents render <json_file> [options] # Render HTML from JSON
|
|
10
|
+
# rspec-agents worker # Internal: worker mode
|
|
11
|
+
|
|
12
|
+
# macOS fork safety - must be set before any Objective-C code loads
|
|
13
|
+
ENV["OBJC_DISABLE_INITIALIZE_FORK_SAFETY"] = "YES"
|
|
14
|
+
|
|
15
|
+
# Sync output streams for worker mode
|
|
16
|
+
$stdout.sync = true
|
|
17
|
+
$stderr.sync = true
|
|
18
|
+
|
|
19
|
+
require "rspec/agents"
|
|
20
|
+
require "rspec/agents/cli"
|
|
21
|
+
|
|
22
|
+
# Run CLI
|
|
23
|
+
exit_code = RSpec::Agents::CLI.run(ARGV)
|
|
24
|
+
exit(exit_code || 0)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
module Agents
|
|
5
|
+
# Shared backtrace extraction and filtering for spec executors.
|
|
6
|
+
# Included by SpecExecutor (sequential) and HeadlessRunner (parallel worker).
|
|
7
|
+
module BacktraceHelper
|
|
8
|
+
MAX_BACKTRACE_LINES = 10
|
|
9
|
+
|
|
10
|
+
def extract_failure_message(notification)
|
|
11
|
+
if notification.respond_to?(:exception) && notification.exception
|
|
12
|
+
notification.exception.message
|
|
13
|
+
elsif notification.example.execution_result.exception
|
|
14
|
+
notification.example.execution_result.exception.message
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract_backtrace(notification)
|
|
19
|
+
backtrace = if notification.respond_to?(:formatted_backtrace)
|
|
20
|
+
notification.formatted_backtrace
|
|
21
|
+
elsif notification.example.execution_result.exception
|
|
22
|
+
notification.example.execution_result.exception.backtrace
|
|
23
|
+
end
|
|
24
|
+
filter_backtrace(backtrace)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def filter_backtrace(backtrace)
|
|
28
|
+
return nil unless backtrace
|
|
29
|
+
|
|
30
|
+
app_path = File.expand_path(Dir.getwd)
|
|
31
|
+
filtered = backtrace.select { |line|
|
|
32
|
+
# Extract the file path portion (before the first colon, e.g. "/path/to/file.rb:42")
|
|
33
|
+
file_path = line.split(":").first
|
|
34
|
+
expanded = File.expand_path(file_path) rescue file_path
|
|
35
|
+
expanded.start_with?(app_path)
|
|
36
|
+
}.first(MAX_BACKTRACE_LINES)
|
|
37
|
+
|
|
38
|
+
# If nothing matched (e.g. formatted_backtrace already filtered), use as-is
|
|
39
|
+
filtered.empty? ? backtrace.first(MAX_BACKTRACE_LINES) : filtered
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/rspec/agents/cli.rb
CHANGED
|
@@ -91,16 +91,17 @@ module RSpec
|
|
|
91
91
|
options = parse_single_options(args)
|
|
92
92
|
|
|
93
93
|
runner = Runners::TerminalRunner.new(
|
|
94
|
-
output:
|
|
95
|
-
color:
|
|
96
|
-
json_path:
|
|
97
|
-
html_path:
|
|
94
|
+
output: $stdout,
|
|
95
|
+
color: options[:color],
|
|
96
|
+
json_path: options[:json_path],
|
|
97
|
+
html_path: options[:html_path],
|
|
98
|
+
upload_url: options[:upload_url]
|
|
98
99
|
)
|
|
99
100
|
runner.run(options[:paths])
|
|
100
101
|
end
|
|
101
102
|
|
|
102
103
|
def parse_single_options(args)
|
|
103
|
-
options = { paths: [], color: nil, json_path: nil, html_path: nil }
|
|
104
|
+
options = { paths: [], color: nil, json_path: nil, html_path: nil, upload_url: nil }
|
|
104
105
|
|
|
105
106
|
parser = OptionParser.new do |opts|
|
|
106
107
|
opts.banner = "Usage: rspec-agents [run] [options] [paths...]"
|
|
@@ -126,6 +127,10 @@ module RSpec
|
|
|
126
127
|
options[:html_path] = path
|
|
127
128
|
end
|
|
128
129
|
|
|
130
|
+
opts.on("--upload [URL]", "Upload run data to agents-studio (default: http://localhost:9292)") do |url|
|
|
131
|
+
options[:upload_url] = url || "http://localhost:9292"
|
|
132
|
+
end
|
|
133
|
+
|
|
129
134
|
opts.on("-h", "--help", "Show this help") do
|
|
130
135
|
puts opts
|
|
131
136
|
exit 0
|
|
@@ -151,20 +156,22 @@ module RSpec
|
|
|
151
156
|
color: options[:color],
|
|
152
157
|
json_path: options[:json_path],
|
|
153
158
|
html_path: options[:html_path],
|
|
154
|
-
ui_mode: options[:ui_mode]
|
|
159
|
+
ui_mode: options[:ui_mode],
|
|
160
|
+
upload_url: options[:upload_url]
|
|
155
161
|
)
|
|
156
162
|
runner.run(options[:paths])
|
|
157
163
|
end
|
|
158
164
|
|
|
159
165
|
def parse_parallel_options(args)
|
|
160
166
|
options = {
|
|
161
|
-
workers:
|
|
162
|
-
fail_fast:
|
|
163
|
-
paths:
|
|
164
|
-
color:
|
|
165
|
-
json_path:
|
|
166
|
-
html_path:
|
|
167
|
-
ui_mode:
|
|
167
|
+
workers: 4,
|
|
168
|
+
fail_fast: false,
|
|
169
|
+
paths: [],
|
|
170
|
+
color: nil,
|
|
171
|
+
json_path: nil,
|
|
172
|
+
html_path: nil,
|
|
173
|
+
ui_mode: nil,
|
|
174
|
+
upload_url: nil
|
|
168
175
|
}
|
|
169
176
|
|
|
170
177
|
parser = OptionParser.new do |opts|
|
|
@@ -199,6 +206,10 @@ module RSpec
|
|
|
199
206
|
options[:html_path] = path
|
|
200
207
|
end
|
|
201
208
|
|
|
209
|
+
opts.on("--upload [URL]", "Upload run data to agents-studio (default: http://localhost:9292)") do |url|
|
|
210
|
+
options[:upload_url] = url || "http://localhost:9292"
|
|
211
|
+
end
|
|
212
|
+
|
|
202
213
|
opts.on("-h", "--help", "Show this help") do
|
|
203
214
|
puts opts
|
|
204
215
|
exit 0
|
|
@@ -9,22 +9,15 @@ module RSpec
|
|
|
9
9
|
@context = context
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# Get the shared turn executor from the test context
|
|
13
13
|
# @return [TurnExecutor]
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
agent: @context.build_agent,
|
|
17
|
-
conversation: @context.conversation,
|
|
18
|
-
graph: @context.topic_graph,
|
|
19
|
-
judge: @context.build_judge(@context.build_llm),
|
|
20
|
-
event_bus: @context.event_bus
|
|
21
|
-
)
|
|
14
|
+
def turn_executor
|
|
15
|
+
@context.turn_executor
|
|
22
16
|
end
|
|
23
17
|
|
|
24
18
|
# Build an agent proxy for assertions
|
|
25
|
-
# @param turn_executor [TurnExecutor]
|
|
26
19
|
# @return [AgentProxy]
|
|
27
|
-
def build_agent_proxy
|
|
20
|
+
def build_agent_proxy
|
|
28
21
|
AgentProxy.new(
|
|
29
22
|
turn_executor: turn_executor,
|
|
30
23
|
judge: @context.build_judge(@context.build_llm)
|
|
@@ -37,13 +30,12 @@ module RSpec
|
|
|
37
30
|
def build_user_simulator(simulator_config)
|
|
38
31
|
llm = @context.build_llm
|
|
39
32
|
Runners::UserSimulator.new(
|
|
40
|
-
|
|
33
|
+
turn_executor: turn_executor,
|
|
41
34
|
llm: llm,
|
|
42
35
|
judge: @context.build_judge(llm),
|
|
43
36
|
graph: @context.topic_graph,
|
|
44
37
|
simulator_config: simulator_config,
|
|
45
|
-
event_bus: @context.event_bus
|
|
46
|
-
conversation: @context.conversation
|
|
38
|
+
event_bus: @context.event_bus
|
|
47
39
|
)
|
|
48
40
|
end
|
|
49
41
|
end
|
|
@@ -55,6 +55,10 @@ module RSpec
|
|
|
55
55
|
# end
|
|
56
56
|
# end
|
|
57
57
|
def scenario_set(name, from: nil, scenarios: nil, &block)
|
|
58
|
+
# Capture the caller location so examples get the correct source location
|
|
59
|
+
# instead of pointing to this DSL file
|
|
60
|
+
scenario_set_caller = caller
|
|
61
|
+
|
|
58
62
|
# Validate arguments - must provide either from: or scenarios:, but not both
|
|
59
63
|
if from.nil? && scenarios.nil?
|
|
60
64
|
raise RSpec::Core::ExampleGroup::WrongScopeError,
|
|
@@ -92,6 +96,10 @@ module RSpec
|
|
|
92
96
|
# Add scenario to metadata
|
|
93
97
|
metadata[:rspec_agents_scenario] = captured_scenario
|
|
94
98
|
|
|
99
|
+
# Pass the caller from scenario_set so RSpec captures the correct
|
|
100
|
+
# source location (the spec file) instead of this DSL file
|
|
101
|
+
metadata[:caller] = scenario_set_caller
|
|
102
|
+
|
|
95
103
|
# Create a wrapped test block that sets the instance variable
|
|
96
104
|
wrapped_test_block = proc do
|
|
97
105
|
@rspec_agents_current_scenario = captured_scenario
|
|
@@ -82,9 +82,7 @@ module RSpec
|
|
|
82
82
|
@criteria = collect_inherited(:criteria)
|
|
83
83
|
@simulator_config = build_simulator_config
|
|
84
84
|
@topic_graph = nil
|
|
85
|
-
|
|
86
|
-
# Otherwise use the singleton EventBus
|
|
87
|
-
@event_bus = Thread.current[:rspec_agents_event_bus] || EventBus.instance
|
|
85
|
+
@event_bus = EventBus.current? ? EventBus.current : EventBus.new
|
|
88
86
|
@conversation = Conversation.new(event_bus: @event_bus)
|
|
89
87
|
end
|
|
90
88
|
|
|
@@ -110,6 +108,18 @@ module RSpec
|
|
|
110
108
|
Judge.new(llm: llm, criteria: @criteria)
|
|
111
109
|
end
|
|
112
110
|
|
|
111
|
+
# Shared turn executor for this test context.
|
|
112
|
+
# @return [TurnExecutor]
|
|
113
|
+
def turn_executor
|
|
114
|
+
@turn_executor ||= TurnExecutor.new(
|
|
115
|
+
agent: build_agent,
|
|
116
|
+
conversation: @conversation,
|
|
117
|
+
graph: @topic_graph,
|
|
118
|
+
judge: build_judge(build_llm),
|
|
119
|
+
event_bus: @event_bus
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
113
123
|
# Merge additional simulator config (from test-level block)
|
|
114
124
|
# @param override [SimulatorConfig]
|
|
115
125
|
# @return [SimulatorConfig]
|
data/lib/rspec/agents/dsl.rb
CHANGED
|
@@ -232,14 +232,15 @@ module RSpec
|
|
|
232
232
|
def ensure_turn_executor_initialized
|
|
233
233
|
return if @turn_executor
|
|
234
234
|
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
context = rspec_agents_test_context
|
|
236
|
+
factory = RunnerFactory.new(context)
|
|
237
|
+
@turn_executor = context.turn_executor
|
|
237
238
|
@user_proxy = UserProxy.new(
|
|
238
|
-
context:
|
|
239
|
+
context: context,
|
|
239
240
|
turn_executor: @turn_executor,
|
|
240
241
|
runner_factory: factory
|
|
241
242
|
)
|
|
242
|
-
@agent_proxy = factory.build_agent_proxy
|
|
243
|
+
@agent_proxy = factory.build_agent_proxy
|
|
243
244
|
end
|
|
244
245
|
end
|
|
245
246
|
|
|
@@ -1,11 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module RSpec
|
|
4
4
|
module Agents
|
|
5
|
-
# Central publish/subscribe hub for events
|
|
6
|
-
# Thread-safe with error isolation (observer errors never fail tests)
|
|
5
|
+
# Central publish/subscribe hub for events.
|
|
6
|
+
# Thread-safe with error isolation (observer errors never fail tests).
|
|
7
|
+
#
|
|
8
|
+
# Access the current thread's event bus via EventBus.current.
|
|
9
|
+
# Each thread (or worker process) gets its own independent instance.
|
|
10
|
+
#
|
|
11
|
+
# @example Setup in a runner/executor
|
|
12
|
+
# EventBus.current = EventBus.new
|
|
13
|
+
# # ... run specs ...
|
|
14
|
+
# EventBus.current = nil # cleanup
|
|
15
|
+
#
|
|
16
|
+
# @example Consuming in observers
|
|
17
|
+
# EventBus.current.add_observer(self)
|
|
18
|
+
#
|
|
7
19
|
class EventBus
|
|
8
|
-
|
|
20
|
+
# Get the event bus for the current thread.
|
|
21
|
+
# Raises if no bus has been set up.
|
|
22
|
+
#
|
|
23
|
+
# @return [EventBus]
|
|
24
|
+
def self.current
|
|
25
|
+
Thread.current[:rspec_agents_event_bus] or
|
|
26
|
+
raise "No EventBus set for current thread. Call EventBus.current = EventBus.new first."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Set the event bus for the current thread.
|
|
30
|
+
#
|
|
31
|
+
# @param bus [EventBus, nil]
|
|
32
|
+
def self.current=(bus)
|
|
33
|
+
Thread.current[:rspec_agents_event_bus] = bus
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check whether an event bus has been set for the current thread.
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def self.current?
|
|
40
|
+
!Thread.current[:rspec_agents_event_bus].nil?
|
|
41
|
+
end
|
|
9
42
|
|
|
10
43
|
def initialize
|
|
11
44
|
@subscribers = Hash.new { |h, k| h[k] = [] }
|
|
@@ -29,7 +29,7 @@ module RSpec
|
|
|
29
29
|
# @param output [IO] Output stream (default: $stdout)
|
|
30
30
|
# @param color [Boolean, nil] Force color on/off (default: auto-detect)
|
|
31
31
|
# @param event_bus [EventBus] Event bus to subscribe to
|
|
32
|
-
def initialize(output: $stdout, color: nil, event_bus:
|
|
32
|
+
def initialize(output: $stdout, color: nil, event_bus:)
|
|
33
33
|
@output = output
|
|
34
34
|
@color = color.nil? ? output.respond_to?(:tty?) && output.tty? : color
|
|
35
35
|
@failures = []
|
|
@@ -160,6 +160,11 @@ module RSpec
|
|
|
160
160
|
append_to_buffer(worker, " #{colorize(line.chomp, :red)}")
|
|
161
161
|
end
|
|
162
162
|
end
|
|
163
|
+
if event.backtrace&.any?
|
|
164
|
+
event.backtrace.first(3).each do |line|
|
|
165
|
+
append_to_buffer(worker, " #{colorize(line, :dim)}")
|
|
166
|
+
end
|
|
167
|
+
end
|
|
163
168
|
|
|
164
169
|
when Events::ExamplePending
|
|
165
170
|
@worker_status[worker] = :pending
|
|
@@ -69,6 +69,11 @@ module RSpec
|
|
|
69
69
|
@output.puts "#{prefix} #{colorize(line.chomp, :red)}"
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
|
+
if event.backtrace&.any?
|
|
73
|
+
event.backtrace.first(3).each do |line|
|
|
74
|
+
@output.puts "#{prefix} #{colorize(line, :dim)}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
72
77
|
|
|
73
78
|
when Events::ExamplePending
|
|
74
79
|
message = event.message ? " (#{event.message})" : ""
|
|
@@ -20,6 +20,7 @@ module RSpec
|
|
|
20
20
|
# EventBus → RpcNotifyObserver → JSON over RPC socket
|
|
21
21
|
#
|
|
22
22
|
class HeadlessRunner
|
|
23
|
+
include BacktraceHelper
|
|
23
24
|
# RSpec notifications we subscribe to
|
|
24
25
|
NOTIFICATIONS = [
|
|
25
26
|
:start,
|
|
@@ -48,8 +49,8 @@ module RSpec
|
|
|
48
49
|
@example_count = 0
|
|
49
50
|
@failure_count = 0
|
|
50
51
|
|
|
51
|
-
# Create
|
|
52
|
-
@event_bus =
|
|
52
|
+
# Create EventBus for this worker
|
|
53
|
+
@event_bus = EventBus.new
|
|
53
54
|
|
|
54
55
|
# Set up RPC forwarding - all events go through this observer
|
|
55
56
|
Observers::RpcNotifyObserver.new(
|
|
@@ -84,15 +85,15 @@ module RSpec
|
|
|
84
85
|
# Register ourselves as a listener for RSpec lifecycle events
|
|
85
86
|
RSpec.configuration.reporter.register_listener(self, *NOTIFICATIONS)
|
|
86
87
|
|
|
87
|
-
#
|
|
88
|
-
|
|
88
|
+
# Set event bus for the current thread so Conversation finds it
|
|
89
|
+
EventBus.current = @event_bus
|
|
89
90
|
|
|
90
91
|
# Run specs
|
|
91
92
|
runner = RSpec::Core::Runner.new(options)
|
|
92
93
|
exit_code = runner.run($stderr, null_output)
|
|
93
94
|
|
|
94
95
|
# Clean up thread-locals
|
|
95
|
-
|
|
96
|
+
EventBus.current = nil
|
|
96
97
|
Thread.current[:rspec_agents_example_id] = nil
|
|
97
98
|
|
|
98
99
|
{
|
|
@@ -241,31 +242,6 @@ module RSpec
|
|
|
241
242
|
nil
|
|
242
243
|
end
|
|
243
244
|
|
|
244
|
-
def extract_failure_message(notification)
|
|
245
|
-
if notification.respond_to?(:exception) && notification.exception
|
|
246
|
-
notification.exception.message
|
|
247
|
-
elsif notification.example.execution_result.exception
|
|
248
|
-
notification.example.execution_result.exception.message
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def extract_backtrace(notification)
|
|
253
|
-
backtrace = if notification.respond_to?(:formatted_backtrace)
|
|
254
|
-
notification.formatted_backtrace
|
|
255
|
-
elsif notification.example.execution_result.exception
|
|
256
|
-
notification.example.execution_result.exception.backtrace
|
|
257
|
-
end
|
|
258
|
-
filter_backtrace(backtrace)
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
def filter_backtrace(backtrace)
|
|
262
|
-
return nil unless backtrace
|
|
263
|
-
|
|
264
|
-
# Include only lines from the current working directory (application code)
|
|
265
|
-
# This matches RSpec's backtrace_inclusion_patterns approach
|
|
266
|
-
app_path = Dir.getwd
|
|
267
|
-
backtrace.select { |line| line.start_with?(app_path) }.first(10)
|
|
268
|
-
end
|
|
269
245
|
end
|
|
270
246
|
end
|
|
271
247
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "async"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require_relative "../parallel/ui/ui_factory"
|
|
6
|
+
require_relative "run_data_uploader"
|
|
6
7
|
|
|
7
8
|
module RSpec
|
|
8
9
|
module Agents
|
|
@@ -43,7 +44,7 @@ module RSpec
|
|
|
43
44
|
# @param html_path [String, nil] Path to save HTML report
|
|
44
45
|
# @param ui_mode [Symbol, nil] Output mode (:interactive, :interleaved, :quiet)
|
|
45
46
|
def initialize(worker_count:, fail_fast: false, output: $stdout, color: nil,
|
|
46
|
-
json_path: nil, html_path: nil, ui_mode: nil)
|
|
47
|
+
json_path: nil, html_path: nil, ui_mode: nil, upload_url: nil)
|
|
47
48
|
@worker_count = worker_count
|
|
48
49
|
@fail_fast = fail_fast
|
|
49
50
|
@output = output
|
|
@@ -51,6 +52,7 @@ module RSpec
|
|
|
51
52
|
@mutex = Mutex.new
|
|
52
53
|
@json_path = json_path
|
|
53
54
|
@html_path = html_path
|
|
55
|
+
@upload_url = upload_url
|
|
54
56
|
|
|
55
57
|
@ui = Parallel::UI::UIFactory.create(
|
|
56
58
|
mode: ui_mode,
|
|
@@ -84,7 +86,7 @@ module RSpec
|
|
|
84
86
|
end
|
|
85
87
|
end
|
|
86
88
|
rescue Parallel::ExampleDiscovery::DiscoveryError => e
|
|
87
|
-
@output.puts colorize("
|
|
89
|
+
@output.puts colorize("Example discovery failed: #{e.message}", :red)
|
|
88
90
|
return 1
|
|
89
91
|
end
|
|
90
92
|
|
|
@@ -113,7 +115,7 @@ module RSpec
|
|
|
113
115
|
end
|
|
114
116
|
|
|
115
117
|
# Save outputs
|
|
116
|
-
save_outputs(executor.run_data) if @json_path || @html_path
|
|
118
|
+
save_outputs(executor.run_data) if @json_path || @html_path || @upload_url
|
|
117
119
|
|
|
118
120
|
result&.success? ? 0 : 1
|
|
119
121
|
ensure
|
|
@@ -192,7 +194,7 @@ module RSpec
|
|
|
192
194
|
failures.each do |event|
|
|
193
195
|
location = event.location
|
|
194
196
|
description = event.full_description || event.description
|
|
195
|
-
@output.puts colorize("
|
|
197
|
+
@output.puts colorize("rspec-agents #{location}", :red) + " " + colorize("# #{description}", :dim)
|
|
196
198
|
end
|
|
197
199
|
@output.puts
|
|
198
200
|
end
|
|
@@ -206,6 +208,7 @@ module RSpec
|
|
|
206
208
|
return unless run_data
|
|
207
209
|
|
|
208
210
|
if @json_path
|
|
211
|
+
populate_rendered_extensions(run_data)
|
|
209
212
|
FileUtils.mkdir_p(File.dirname(@json_path))
|
|
210
213
|
Serialization::JsonFile.write(@json_path, run_data)
|
|
211
214
|
end
|
|
@@ -213,6 +216,27 @@ module RSpec
|
|
|
213
216
|
if @html_path
|
|
214
217
|
Serialization::TestSuiteRenderer.render(run_data, output_path: @html_path)
|
|
215
218
|
end
|
|
219
|
+
|
|
220
|
+
if @upload_url
|
|
221
|
+
RunDataUploader.new(url: @upload_url, output: @output).upload(run_data)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def populate_rendered_extensions(run_data)
|
|
226
|
+
extensions = [Serialization::Extensions::CoreExtension] +
|
|
227
|
+
RSpec::Agents.configuration.html_extensions
|
|
228
|
+
run_data.examples.each_value do |example|
|
|
229
|
+
next unless example.conversation
|
|
230
|
+
|
|
231
|
+
renderer = Serialization::ConversationRenderer.new(
|
|
232
|
+
example.conversation,
|
|
233
|
+
extensions: extensions,
|
|
234
|
+
example_id: example.stable_id || example.id
|
|
235
|
+
)
|
|
236
|
+
example.rendered_extensions = renderer.build_rendered_extensions
|
|
237
|
+
end
|
|
238
|
+
rescue StandardError => e
|
|
239
|
+
warn "Warning: Failed to generate rendered_extensions: #{e.message}"
|
|
216
240
|
end
|
|
217
241
|
end
|
|
218
242
|
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module RSpec
|
|
8
|
+
module Agents
|
|
9
|
+
module Runners
|
|
10
|
+
# Uploads run data to an agents-studio webapp via HTTP POST.
|
|
11
|
+
# Used by TerminalRunner and ParallelTerminalRunner when --upload is specified.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# uploader = RunDataUploader.new(url: "http://localhost:9292")
|
|
15
|
+
# uploader.upload(run_data) # => true/false
|
|
16
|
+
#
|
|
17
|
+
class RunDataUploader
|
|
18
|
+
TIMEOUT = 30 # seconds
|
|
19
|
+
|
|
20
|
+
# @param url [String] Base URL of the agents-studio webapp
|
|
21
|
+
# @param output [IO] Output stream for status messages
|
|
22
|
+
def initialize(url:, output: $stdout)
|
|
23
|
+
@url = url.chomp("/")
|
|
24
|
+
@output = output
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Upload run data to the webapp.
|
|
28
|
+
# @param run_data [Serialization::RunData] The run data to upload
|
|
29
|
+
# @return [Boolean] true if upload succeeded
|
|
30
|
+
def upload(run_data)
|
|
31
|
+
return false unless run_data
|
|
32
|
+
|
|
33
|
+
uri = URI.parse("#{@url}/api/import")
|
|
34
|
+
json_body = JSON.generate(run_data.to_h)
|
|
35
|
+
|
|
36
|
+
@output.puts "Uploading run data to #{@url}..."
|
|
37
|
+
|
|
38
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
39
|
+
http.use_ssl = (uri.scheme == "https")
|
|
40
|
+
http.open_timeout = TIMEOUT
|
|
41
|
+
http.read_timeout = TIMEOUT
|
|
42
|
+
|
|
43
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
44
|
+
request["Content-Type"] = "application/json"
|
|
45
|
+
request.body = json_body
|
|
46
|
+
|
|
47
|
+
response = http.request(request)
|
|
48
|
+
|
|
49
|
+
if response.is_a?(Net::HTTPSuccess)
|
|
50
|
+
result = JSON.parse(response.body)
|
|
51
|
+
@output.puts "Upload complete: #{result["example_count"]} examples " \
|
|
52
|
+
"(#{result["passed_count"]} passed, #{result["failed_count"]} failed)"
|
|
53
|
+
true
|
|
54
|
+
else
|
|
55
|
+
@output.puts "Upload failed (HTTP #{response.code}): #{response.body}"
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
rescue Errno::ECONNREFUSED
|
|
59
|
+
@output.puts "Upload failed: could not connect to #{@url} (is agents-studio running?)"
|
|
60
|
+
false
|
|
61
|
+
rescue Errno::ETIMEDOUT, Net::OpenTimeout, Net::ReadTimeout
|
|
62
|
+
@output.puts "Upload failed: connection to #{@url} timed out"
|
|
63
|
+
false
|
|
64
|
+
rescue StandardError => e
|
|
65
|
+
@output.puts "Upload failed: #{e.message}"
|
|
66
|
+
false
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|