rspec-agents 0.1.2 → 0.1.3
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 +1 -2
- data/lib/rspec/agents/cli.rb +48 -120
- data/lib/rspec/agents/runners/parallel_terminal_runner.rb +21 -7
- data/lib/rspec/agents/runners/streaming_run_data_uploader.rb +195 -0
- data/lib/rspec/agents/runners/terminal_runner.rb +21 -7
- data/lib/rspec/agents/turn_executor.rb +8 -0
- data/lib/rspec/agents/version.rb +1 -1
- metadata +2 -2
- data/lib/rspec/agents/runners/run_data_uploader.rb +0 -71
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f32aaad8fd16cbd4acd7406452a9b070fbde786ce582fe82964ed846a9547420
|
|
4
|
+
data.tar.gz: 8132da0b079c695d7f763d22bb65527672856dd1b7f4acf1659f5f580afd1769
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 102605b1124b6b0c0f052465e3347ccd5779e3218c797e909a702c8ff282fca8b015100b8002f830637cfe38c55a9e2defe7447dcff0a3f06601c61d9de56141
|
|
7
|
+
data.tar.gz: 732cc6a14e67137fadfaf66bd960a81443a500dada2f206ab462220d8945543594fe0777fe6eac3c061210ca664e8a399a5bb5df087a5283a7187ce927589d9e
|
data/bin/rspec-agents
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
# Unified CLI for rspec-agents
|
|
5
5
|
#
|
|
6
6
|
# Usage:
|
|
7
|
-
# rspec-agents [
|
|
8
|
-
# rspec-agents parallel [options] [paths...] # Parallel with workers
|
|
7
|
+
# rspec-agents [options] [paths...] # Run specs (use -w for parallel)
|
|
9
8
|
# rspec-agents render <json_file> [options] # Render HTML from JSON
|
|
10
9
|
# rspec-agents worker # Internal: worker mode
|
|
11
10
|
|
data/lib/rspec/agents/cli.rb
CHANGED
|
@@ -6,23 +6,24 @@ module RSpec
|
|
|
6
6
|
module Agents
|
|
7
7
|
# Unified CLI for rspec-agents
|
|
8
8
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
9
|
+
# Subcommands:
|
|
10
|
+
# render - Generate HTML report from JSON file
|
|
11
|
+
# worker - Internal: run as a worker subprocess
|
|
12
|
+
#
|
|
13
|
+
# Without a subcommand, runs specs directly. Pass -w/--workers to
|
|
14
|
+
# enable parallel execution.
|
|
13
15
|
#
|
|
14
16
|
# @example Single process
|
|
15
17
|
# CLI.run(["spec/"])
|
|
16
|
-
# CLI.run(["run", "spec/"])
|
|
17
18
|
#
|
|
18
19
|
# @example Parallel
|
|
19
|
-
# CLI.run(["
|
|
20
|
+
# CLI.run(["-w", "4", "spec/"])
|
|
20
21
|
#
|
|
21
22
|
# @example Worker (internal)
|
|
22
23
|
# CLI.run(["worker"])
|
|
23
24
|
#
|
|
24
25
|
class CLI
|
|
25
|
-
|
|
26
|
+
SUBCOMMANDS = %w[worker render].freeze
|
|
26
27
|
|
|
27
28
|
def self.run(argv)
|
|
28
29
|
new(argv).run
|
|
@@ -33,159 +34,86 @@ module RSpec
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def run
|
|
36
|
-
command, args =
|
|
37
|
+
command, args = extract_subcommand(@argv)
|
|
37
38
|
|
|
38
39
|
case command
|
|
39
|
-
when "run"
|
|
40
|
-
run_single(args)
|
|
41
|
-
when "parallel"
|
|
42
|
-
run_parallel(args)
|
|
43
40
|
when "worker"
|
|
44
41
|
run_worker(args)
|
|
45
42
|
when "render"
|
|
46
43
|
run_render(args)
|
|
47
44
|
else
|
|
48
|
-
|
|
49
|
-
run_single(@argv)
|
|
45
|
+
run_specs(args)
|
|
50
46
|
end
|
|
51
47
|
end
|
|
52
48
|
|
|
53
49
|
private
|
|
54
50
|
|
|
55
|
-
def
|
|
51
|
+
def extract_subcommand(argv)
|
|
56
52
|
return [nil, argv] if argv.empty?
|
|
57
53
|
|
|
58
54
|
first = argv.first
|
|
59
|
-
|
|
60
|
-
# Auto-detect parallel mode if -w/--workers flag is present
|
|
61
|
-
if has_parallel_flag?(argv)
|
|
62
|
-
# If first arg is explicit command, consume it; otherwise keep all args
|
|
63
|
-
if COMMANDS.include?(first)
|
|
64
|
-
return ["parallel", argv[1..]]
|
|
65
|
-
else
|
|
66
|
-
return ["parallel", argv]
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Original logic for explicit commands or default to single-process
|
|
71
|
-
if COMMANDS.include?(first)
|
|
55
|
+
if SUBCOMMANDS.include?(first)
|
|
72
56
|
[first, argv[1..]]
|
|
73
|
-
elsif first.start_with?("-")
|
|
74
|
-
# Flag, not a command - default to run
|
|
75
|
-
[nil, argv]
|
|
76
57
|
else
|
|
77
|
-
# Path or unknown - default to run
|
|
78
58
|
[nil, argv]
|
|
79
59
|
end
|
|
80
60
|
end
|
|
81
61
|
|
|
82
|
-
def has_parallel_flag?(argv)
|
|
83
|
-
argv.any? { |arg| arg == "-w" || arg.start_with?("--workers") }
|
|
84
|
-
end
|
|
85
|
-
|
|
86
62
|
# =========================================================================
|
|
87
|
-
#
|
|
63
|
+
# Spec execution (single-process or parallel via -w)
|
|
88
64
|
# =========================================================================
|
|
89
65
|
|
|
90
|
-
def
|
|
91
|
-
options =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
opts.on("--[no-]color", "Force color on/off (default: auto)") do |v|
|
|
114
|
-
options[:color] = v
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
opts.on("--ui MODE", [:interactive, :interleaved, :quiet],
|
|
118
|
-
"Output mode (ignored in single-process mode)") do |_mode|
|
|
119
|
-
# Accepted for CLI compatibility with parallel mode, but ignored
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
opts.on("--json PATH", "Save JSON run data to file") do |path|
|
|
123
|
-
options[:json_path] = path
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
opts.on("--html PATH", "Render HTML report to path") do |path|
|
|
127
|
-
options[:html_path] = path
|
|
128
|
-
end
|
|
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
|
-
|
|
134
|
-
opts.on("-h", "--help", "Show this help") do
|
|
135
|
-
puts opts
|
|
136
|
-
exit 0
|
|
137
|
-
end
|
|
66
|
+
def run_specs(args)
|
|
67
|
+
options = parse_run_options(args)
|
|
68
|
+
|
|
69
|
+
if options[:workers]
|
|
70
|
+
runner = Runners::ParallelTerminalRunner.new(
|
|
71
|
+
worker_count: options[:workers],
|
|
72
|
+
fail_fast: options[:fail_fast],
|
|
73
|
+
output: $stdout,
|
|
74
|
+
color: options[:color],
|
|
75
|
+
json_path: options[:json_path],
|
|
76
|
+
html_path: options[:html_path],
|
|
77
|
+
ui_mode: options[:ui_mode],
|
|
78
|
+
upload_url: options[:upload_url]
|
|
79
|
+
)
|
|
80
|
+
else
|
|
81
|
+
runner = Runners::TerminalRunner.new(
|
|
82
|
+
output: $stdout,
|
|
83
|
+
color: options[:color],
|
|
84
|
+
json_path: options[:json_path],
|
|
85
|
+
html_path: options[:html_path],
|
|
86
|
+
upload_url: options[:upload_url]
|
|
87
|
+
)
|
|
138
88
|
end
|
|
139
89
|
|
|
140
|
-
remaining = parser.parse(args)
|
|
141
|
-
options[:paths] = remaining.empty? ? ["spec"] : remaining
|
|
142
|
-
options
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# =========================================================================
|
|
146
|
-
# Parallel mode
|
|
147
|
-
# =========================================================================
|
|
148
|
-
|
|
149
|
-
def run_parallel(args)
|
|
150
|
-
options = parse_parallel_options(args)
|
|
151
|
-
|
|
152
|
-
runner = Runners::ParallelTerminalRunner.new(
|
|
153
|
-
worker_count: options[:workers],
|
|
154
|
-
fail_fast: options[:fail_fast],
|
|
155
|
-
output: $stdout,
|
|
156
|
-
color: options[:color],
|
|
157
|
-
json_path: options[:json_path],
|
|
158
|
-
html_path: options[:html_path],
|
|
159
|
-
ui_mode: options[:ui_mode],
|
|
160
|
-
upload_url: options[:upload_url]
|
|
161
|
-
)
|
|
162
90
|
runner.run(options[:paths])
|
|
163
91
|
end
|
|
164
92
|
|
|
165
|
-
def
|
|
93
|
+
def parse_run_options(args)
|
|
166
94
|
options = {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
color:
|
|
171
|
-
json_path:
|
|
172
|
-
html_path:
|
|
173
|
-
ui_mode:
|
|
95
|
+
paths: [],
|
|
96
|
+
workers: nil,
|
|
97
|
+
fail_fast: false,
|
|
98
|
+
color: nil,
|
|
99
|
+
json_path: nil,
|
|
100
|
+
html_path: nil,
|
|
101
|
+
ui_mode: nil,
|
|
174
102
|
upload_url: nil
|
|
175
103
|
}
|
|
176
104
|
|
|
177
105
|
parser = OptionParser.new do |opts|
|
|
178
|
-
opts.banner = "Usage: rspec-agents
|
|
106
|
+
opts.banner = "Usage: rspec-agents [options] [paths...]"
|
|
179
107
|
opts.separator ""
|
|
180
|
-
opts.separator "Run specs
|
|
108
|
+
opts.separator "Run specs with terminal output. Pass -w to run in parallel."
|
|
181
109
|
opts.separator ""
|
|
182
110
|
opts.separator "Options:"
|
|
183
111
|
|
|
184
|
-
opts.on("-w", "--workers COUNT", Integer, "
|
|
112
|
+
opts.on("-w", "--workers COUNT", Integer, "Run in parallel with COUNT workers") do |w|
|
|
185
113
|
options[:workers] = w
|
|
186
114
|
end
|
|
187
115
|
|
|
188
|
-
opts.on("--fail-fast", "Stop on first failure") do
|
|
116
|
+
opts.on("--fail-fast", "Stop on first failure (parallel mode)") do
|
|
189
117
|
options[:fail_fast] = true
|
|
190
118
|
end
|
|
191
119
|
|
|
@@ -194,7 +122,7 @@ module RSpec
|
|
|
194
122
|
end
|
|
195
123
|
|
|
196
124
|
opts.on("--ui MODE", [:interactive, :interleaved, :quiet],
|
|
197
|
-
"Output mode: interactive, interleaved, quiet (default: auto)") do |mode|
|
|
125
|
+
"Output mode: interactive, interleaved, quiet (parallel mode, default: auto)") do |mode|
|
|
198
126
|
options[:ui_mode] = mode
|
|
199
127
|
end
|
|
200
128
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "async"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
require_relative "../parallel/ui/ui_factory"
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "streaming_run_data_uploader"
|
|
7
7
|
|
|
8
8
|
module RSpec
|
|
9
9
|
module Agents
|
|
@@ -94,6 +94,12 @@ module RSpec
|
|
|
94
94
|
executor.on_event { |type, event| route_event_to_ui(type, event) }
|
|
95
95
|
executor.on_progress { |c, t, f| @ui.on_progress(completed: c, total: t, failures: f) }
|
|
96
96
|
|
|
97
|
+
# Wire up streaming upload if --upload specified
|
|
98
|
+
if @upload_url
|
|
99
|
+
streaming_uploader = StreamingRunDataUploader.new(url: @upload_url, output: @output)
|
|
100
|
+
attach_streaming_upload(streaming_uploader, executor)
|
|
101
|
+
end
|
|
102
|
+
|
|
97
103
|
# Start UI
|
|
98
104
|
@ui.on_run_started(worker_count: @worker_count, example_count: examples.size)
|
|
99
105
|
@ui.start_input_handling
|
|
@@ -114,8 +120,11 @@ module RSpec
|
|
|
114
120
|
print_failed_examples_filter
|
|
115
121
|
end
|
|
116
122
|
|
|
117
|
-
#
|
|
118
|
-
|
|
123
|
+
# Finish streaming upload (blocks until queue drained)
|
|
124
|
+
streaming_uploader&.finish
|
|
125
|
+
|
|
126
|
+
# Save file outputs
|
|
127
|
+
save_outputs(executor.run_data) if @json_path || @html_path
|
|
119
128
|
|
|
120
129
|
result&.success? ? 0 : 1
|
|
121
130
|
ensure
|
|
@@ -204,6 +213,15 @@ module RSpec
|
|
|
204
213
|
"#{COLORS[color]}#{text}#{COLORS[:reset]}"
|
|
205
214
|
end
|
|
206
215
|
|
|
216
|
+
def attach_streaming_upload(uploader, executor)
|
|
217
|
+
executor.on_event do |type, _event|
|
|
218
|
+
uploader.start(executor.run_data) if type == "SuiteStarted"
|
|
219
|
+
end
|
|
220
|
+
executor.on_example_completed do |event, run_data|
|
|
221
|
+
uploader.upload_example(event, run_data)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
207
225
|
def save_outputs(run_data)
|
|
208
226
|
return unless run_data
|
|
209
227
|
|
|
@@ -216,10 +234,6 @@ module RSpec
|
|
|
216
234
|
if @html_path
|
|
217
235
|
Serialization::TestSuiteRenderer.render(run_data, output_path: @html_path)
|
|
218
236
|
end
|
|
219
|
-
|
|
220
|
-
if @upload_url
|
|
221
|
-
RunDataUploader.new(url: @upload_url, output: @output).upload(run_data)
|
|
222
|
-
end
|
|
223
237
|
end
|
|
224
238
|
|
|
225
239
|
def populate_rendered_extensions(run_data)
|
|
@@ -0,0 +1,195 @@
|
|
|
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 incrementally, one example at a time.
|
|
11
|
+
# Each upload sends a minimal RunData containing run metadata + just that example.
|
|
12
|
+
# The receiving /api/import endpoint is idempotent with respect to run_id, merging
|
|
13
|
+
# examples into the same run record.
|
|
14
|
+
#
|
|
15
|
+
# Uses a background thread with a Queue so uploads never block spec execution.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# uploader = StreamingRunDataUploader.new(url: "http://localhost:9292", output: $stdout)
|
|
19
|
+
# uploader.start(run_data)
|
|
20
|
+
# executor.on_example_completed { |event, rd| uploader.upload_example(event, rd) }
|
|
21
|
+
# # ... after suite finishes ...
|
|
22
|
+
# uploader.finish
|
|
23
|
+
#
|
|
24
|
+
class StreamingRunDataUploader
|
|
25
|
+
TIMEOUT = 10 # seconds per request
|
|
26
|
+
|
|
27
|
+
# @param url [String] Base URL of the agents-studio webapp
|
|
28
|
+
# @param output [IO] Output stream for status messages
|
|
29
|
+
def initialize(url:, output: $stdout)
|
|
30
|
+
@url = url.chomp("/")
|
|
31
|
+
@output = output
|
|
32
|
+
@queue = Queue.new
|
|
33
|
+
@failed_examples = {}
|
|
34
|
+
@mutex = Mutex.new
|
|
35
|
+
@worker_thread = nil
|
|
36
|
+
@http = nil
|
|
37
|
+
@uploaded_count = 0
|
|
38
|
+
@run_id = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Whether the uploader has been started.
|
|
42
|
+
def started?
|
|
43
|
+
!!@run_id
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Start the background upload worker.
|
|
47
|
+
# Idempotent — safe to call multiple times; only the first call takes effect.
|
|
48
|
+
# @param run_data [Serialization::RunData] The run data (used for metadata)
|
|
49
|
+
def start(run_data)
|
|
50
|
+
return if started?
|
|
51
|
+
|
|
52
|
+
@run_id = run_data.run_id
|
|
53
|
+
@run_metadata = {
|
|
54
|
+
run_id: run_data.run_id,
|
|
55
|
+
started_at: run_data.started_at,
|
|
56
|
+
seed: run_data.seed,
|
|
57
|
+
git_commit: run_data.git_commit
|
|
58
|
+
}
|
|
59
|
+
@output.puts "Streaming run data to #{@url}..."
|
|
60
|
+
@worker_thread = Thread.new { process_queue }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Enqueue a single example for upload.
|
|
64
|
+
# Meant to be called from an on_example_completed callback.
|
|
65
|
+
# @param event [Events::ExamplePassed, Events::ExampleFailed, Events::ExamplePending]
|
|
66
|
+
# @param run_data [Serialization::RunData] Current run data
|
|
67
|
+
def upload_example(event, run_data)
|
|
68
|
+
return unless @run_id
|
|
69
|
+
|
|
70
|
+
example_data = run_data&.example(event.example_id)
|
|
71
|
+
return unless example_data
|
|
72
|
+
|
|
73
|
+
@queue.push([:upload, { example_id: event.example_id, example_data: example_data }])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Signal the worker to finish: retry failed examples, then drain and stop.
|
|
77
|
+
# Blocks until the worker thread completes (up to 60s).
|
|
78
|
+
def finish
|
|
79
|
+
return unless @worker_thread
|
|
80
|
+
|
|
81
|
+
@queue.push([:finish, nil])
|
|
82
|
+
@worker_thread.join(60)
|
|
83
|
+
close_http
|
|
84
|
+
|
|
85
|
+
if @uploaded_count > 0
|
|
86
|
+
@output.puts "Upload complete: #{@uploaded_count} example#{"s" unless @uploaded_count == 1} streamed"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
if @failed_examples.any?
|
|
90
|
+
@output.puts "Warning: #{@failed_examples.size} example(s) failed to upload"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def process_queue
|
|
97
|
+
loop do
|
|
98
|
+
action, payload = @queue.pop
|
|
99
|
+
|
|
100
|
+
case action
|
|
101
|
+
when :upload
|
|
102
|
+
do_upload_example(payload)
|
|
103
|
+
when :finish
|
|
104
|
+
retry_failed_examples
|
|
105
|
+
break
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
rescue => e
|
|
109
|
+
@output.puts "Upload worker error: #{e.message}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def do_upload_example(payload)
|
|
113
|
+
example_data = payload[:example_data]
|
|
114
|
+
stable_id = example_data.stable_id || payload[:example_id]
|
|
115
|
+
|
|
116
|
+
# Build a minimal RunData with just this one example
|
|
117
|
+
body = build_single_example_payload(example_data)
|
|
118
|
+
post("/api/import", body)
|
|
119
|
+
|
|
120
|
+
@mutex.synchronize do
|
|
121
|
+
@uploaded_count += 1
|
|
122
|
+
@failed_examples.delete(stable_id)
|
|
123
|
+
end
|
|
124
|
+
rescue => e
|
|
125
|
+
@mutex.synchronize do
|
|
126
|
+
@failed_examples[stable_id] = payload
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def retry_failed_examples
|
|
131
|
+
failed = @mutex.synchronize { @failed_examples.dup }
|
|
132
|
+
failed.each do |stable_id, payload|
|
|
133
|
+
do_upload_example(payload)
|
|
134
|
+
rescue
|
|
135
|
+
# Leave in @failed_examples for final warning
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def build_single_example_payload(example_data)
|
|
140
|
+
{
|
|
141
|
+
run_id: @run_metadata[:run_id],
|
|
142
|
+
started_at: serialize_time(@run_metadata[:started_at]),
|
|
143
|
+
seed: @run_metadata[:seed],
|
|
144
|
+
git_commit: @run_metadata[:git_commit],
|
|
145
|
+
scenarios: {},
|
|
146
|
+
examples: {
|
|
147
|
+
example_data.id => example_data.to_h
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def serialize_time(time)
|
|
153
|
+
return nil unless time
|
|
154
|
+
time.is_a?(Time) ? time.iso8601(3) : time
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def http_connection
|
|
158
|
+
@http ||= begin
|
|
159
|
+
uri = URI.parse(@url)
|
|
160
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
161
|
+
http.use_ssl = (uri.scheme == "https")
|
|
162
|
+
http.open_timeout = TIMEOUT
|
|
163
|
+
http.read_timeout = TIMEOUT
|
|
164
|
+
http.start
|
|
165
|
+
http
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def close_http
|
|
170
|
+
@http&.finish
|
|
171
|
+
rescue
|
|
172
|
+
# Ignore close errors
|
|
173
|
+
ensure
|
|
174
|
+
@http = nil
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def post(path, body)
|
|
178
|
+
uri = URI.parse("#{@url}#{path}")
|
|
179
|
+
request = Net::HTTP::Post.new(uri.path)
|
|
180
|
+
request["Content-Type"] = "application/json"
|
|
181
|
+
request.body = JSON.generate(body)
|
|
182
|
+
|
|
183
|
+
response = http_connection.request(request)
|
|
184
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
185
|
+
raise "HTTP #{response.code}: #{response.body}"
|
|
186
|
+
end
|
|
187
|
+
response
|
|
188
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, IOError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
189
|
+
close_http # Reset connection on transport errors
|
|
190
|
+
raise
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
|
-
require_relative "
|
|
4
|
+
require_relative "streaming_run_data_uploader"
|
|
5
5
|
|
|
6
6
|
module RSpec
|
|
7
7
|
module Agents
|
|
@@ -50,10 +50,19 @@ module RSpec
|
|
|
50
50
|
# Wire up event handling for terminal display
|
|
51
51
|
executor.on_event { |type, event| handle_event(type, event) }
|
|
52
52
|
|
|
53
|
+
# Wire up streaming upload if --upload specified
|
|
54
|
+
if @upload_url
|
|
55
|
+
streaming_uploader = StreamingRunDataUploader.new(url: @upload_url, output: @output)
|
|
56
|
+
attach_streaming_upload(streaming_uploader, executor)
|
|
57
|
+
end
|
|
58
|
+
|
|
53
59
|
result = executor.execute(Array(files_or_args))
|
|
54
60
|
|
|
55
|
-
#
|
|
56
|
-
|
|
61
|
+
# Finish streaming upload (blocks until queue drained)
|
|
62
|
+
streaming_uploader&.finish
|
|
63
|
+
|
|
64
|
+
# Save file outputs after run completes
|
|
65
|
+
save_outputs(executor.run_data) if @json_path || @html_path
|
|
57
66
|
|
|
58
67
|
result.success? ? 0 : 1
|
|
59
68
|
end
|
|
@@ -166,6 +175,15 @@ module RSpec
|
|
|
166
175
|
"#{COLORS[color]}#{text}#{COLORS[:reset]}"
|
|
167
176
|
end
|
|
168
177
|
|
|
178
|
+
def attach_streaming_upload(uploader, executor)
|
|
179
|
+
executor.on_event do |type, _event|
|
|
180
|
+
uploader.start(executor.run_data) if type == "SuiteStarted"
|
|
181
|
+
end
|
|
182
|
+
executor.on_example_completed do |event, run_data|
|
|
183
|
+
uploader.upload_example(event, run_data)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
169
187
|
def save_outputs(run_data)
|
|
170
188
|
return unless run_data
|
|
171
189
|
|
|
@@ -178,10 +196,6 @@ module RSpec
|
|
|
178
196
|
if @html_path
|
|
179
197
|
Serialization::TestSuiteRenderer.render(run_data, output_path: @html_path)
|
|
180
198
|
end
|
|
181
|
-
|
|
182
|
-
if @upload_url
|
|
183
|
-
RunDataUploader.new(url: @upload_url, output: @output).upload(run_data)
|
|
184
|
-
end
|
|
185
199
|
end
|
|
186
200
|
|
|
187
201
|
def populate_rendered_extensions(run_data)
|
|
@@ -55,6 +55,14 @@ module RSpec
|
|
|
55
55
|
@conversation.current_topic
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# Get tool calls from the current response, optionally filtered by name
|
|
59
|
+
# @param name [Symbol, String, nil] Optional tool name to filter by
|
|
60
|
+
# @return [Array<ToolCall>]
|
|
61
|
+
def tool_calls(name = nil)
|
|
62
|
+
calls = @current_response&.tool_calls || []
|
|
63
|
+
name ? calls.select { |tc| tc.name == name.to_sym } : calls
|
|
64
|
+
end
|
|
65
|
+
|
|
58
66
|
# Check if current turn is in expected topic
|
|
59
67
|
#
|
|
60
68
|
# @param expected_topic [Symbol] Expected topic name
|
data/lib/rspec/agents/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Frederik Fix
|
|
@@ -186,7 +186,7 @@ files:
|
|
|
186
186
|
- lib/rspec/agents/prompt_builders/user_simulation.rb
|
|
187
187
|
- lib/rspec/agents/runners/headless_runner.rb
|
|
188
188
|
- lib/rspec/agents/runners/parallel_terminal_runner.rb
|
|
189
|
-
- lib/rspec/agents/runners/
|
|
189
|
+
- lib/rspec/agents/runners/streaming_run_data_uploader.rb
|
|
190
190
|
- lib/rspec/agents/runners/terminal_runner.rb
|
|
191
191
|
- lib/rspec/agents/runners/user_simulator.rb
|
|
192
192
|
- lib/rspec/agents/scenario.rb
|
|
@@ -1,71 +0,0 @@
|
|
|
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
|