vercon 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.simplecov +3 -0
- data/README.md +22 -12
- data/Rakefile +3 -3
- data/exe/vercon +1 -1
- data/lib/vercon/claude.rb +38 -16
- data/lib/vercon/cli.rb +5 -5
- data/lib/vercon/commands/generate.rb +106 -66
- data/lib/vercon/commands/init.rb +28 -16
- data/lib/vercon/config.rb +21 -14
- data/lib/vercon/factories.rb +4 -4
- data/lib/vercon/prompt.rb +31 -21
- data/lib/vercon/stdout.rb +8 -8
- data/lib/vercon/version.rb +1 -1
- data/lib/vercon.rb +7 -7
- data/vercon.gemspec +22 -22
- metadata +9 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c277e56e0c21fbb3a590af48d47d65d020fc93df1e1b127ac4e90124426b6b18
|
4
|
+
data.tar.gz: 2a7e8ce4d06bc8ab9cb15708871107ab34249afab8f61709ab798343e0d85242
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b556db258d22cbd92eac0c94fe62c2cee5a7d4995dfe8a6790b97e6c514936cb0e9c4ef03f21f55c2d7b61fa830750c6d1045e3e487a39b0f5a959af8ec40632
|
7
|
+
data.tar.gz: e3dfc208e7bc28f4ba5f8717539f82f52751d086d4ec77c652f06f895d92109bb1a1ff3b2dbfe5a0eb26d0587f92885dcdf3071fcf67b75005cdcde14215786b
|
data/.simplecov
ADDED
data/README.md
CHANGED
@@ -1,34 +1,44 @@
|
|
1
1
|
# Vercon
|
2
2
|
|
3
|
-
|
3
|
+
Vercon - a handy little gem that takes the pain out of writing tests for your Ruby projects. 🔥
|
4
4
|
|
5
|
-
|
5
|
+
The name Vercon comes from the Latin "verum conantur", meaning "they strive for truth". Vercon automatically generate test files and even though they will not always will be perfect, but at least it will save some time writting them manually.
|
6
6
|
|
7
|
-
|
7
|
+
It's build on top of Claude 3. It sends the source code of the Ruby file alongside with available factory names and current test file (in case one exists).
|
8
8
|
|
9
|
-
|
9
|
+
Claude analyzes your code to understand how it works, then uses that knowledge to put together relevant tests.
|
10
10
|
|
11
|
-
|
11
|
+
Just let the AI handle the tedious test writing for you 🚀
|
12
12
|
|
13
|
-
|
13
|
+
Give Vercon a try and save yourself some time and headaches when it comes to testing your Ruby apps.
|
14
14
|
|
15
|
-
|
15
|
+
Easy, efficient, no fuss 👌
|
16
16
|
|
17
|
-
|
17
|
+
## How to use
|
18
18
|
|
19
|
-
|
19
|
+
Install the gem by executing:
|
20
20
|
|
21
|
-
|
21
|
+
$ gem install vercon
|
22
|
+
|
23
|
+
It will require from you Claude Api Token so greb it from [here](https://console.anthropic.com/settings/keys)
|
24
|
+
|
25
|
+
Initialize the gem by executing:
|
26
|
+
|
27
|
+
$ vercon init
|
28
|
+
|
29
|
+
Generate test:
|
30
|
+
|
31
|
+
$ vercon generate <relative ruby file path>
|
22
32
|
|
23
33
|
## Development
|
24
34
|
|
25
35
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
26
36
|
|
27
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
37
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
28
38
|
|
29
39
|
## Contributing
|
30
40
|
|
31
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
41
|
+
Bug reports and pull requests are welcome on GitHub at [vercon repo](https://github.com/AlexBeznoss/vercon).
|
32
42
|
|
33
43
|
## License
|
34
44
|
|
data/Rakefile
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
5
|
|
6
6
|
RSpec::Core::RakeTask.new(:spec)
|
7
7
|
|
8
|
-
require
|
8
|
+
require "standard/rake"
|
9
9
|
|
10
10
|
task default: %i[spec standard]
|
data/exe/vercon
CHANGED
data/lib/vercon/claude.rb
CHANGED
@@ -1,53 +1,75 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "httpx"
|
4
4
|
|
5
5
|
module Vercon
|
6
6
|
class Claude
|
7
|
-
BASE_URL =
|
7
|
+
BASE_URL = "https://api.anthropic.com"
|
8
8
|
|
9
9
|
def initialize
|
10
10
|
config = Vercon::Config.new
|
11
11
|
|
12
|
-
@api_token = config.
|
12
|
+
@api_token = config.claude_token
|
13
13
|
@claude_model = config.claude_model
|
14
14
|
end
|
15
15
|
|
16
16
|
def submit(model: nil, system: nil, max_tokens: 4096, temperature: 0.2, stop_sequences: nil, user: nil, # rubocop:disable Metrics/ParameterLists
|
17
|
-
|
17
|
+
messages: nil, tools: nil)
|
18
18
|
body = {
|
19
19
|
model: model || @claude_model,
|
20
20
|
system: system,
|
21
21
|
max_tokens: max_tokens,
|
22
22
|
temperature: temperature,
|
23
23
|
stop_sequences: stop_sequences,
|
24
|
-
messages: messages || [{
|
25
|
-
|
24
|
+
messages: messages || [{role: "user", content: user}],
|
25
|
+
tools: tools
|
26
|
+
}.reject { |_, v| v.nil? || ["", [], {}].include?(v) }
|
26
27
|
|
27
|
-
client.post(
|
28
|
+
client.post("/v1/messages", body: body.to_json).then { |res| prepare_response(res.json) }
|
28
29
|
end
|
29
30
|
|
30
31
|
private
|
31
32
|
|
32
33
|
def extra_headers
|
33
|
-
{
|
34
|
+
{"x-api-key" => @api_token, "anthropic-version" => "2023-06-01"}
|
34
35
|
end
|
35
36
|
|
36
37
|
def client
|
37
38
|
@client ||=
|
38
39
|
HTTPX
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
.plugin(:retries)
|
41
|
+
.with(headers: {"Content-Type" => "application/json", "Cache-Control" => "no-cache",
|
42
|
+
"anthropic-version" => "2023-06-01", "anthropic-beta" => "tools-2024-04-04"})
|
43
|
+
.with(headers: extra_headers)
|
44
|
+
.with(origin: BASE_URL)
|
45
|
+
.with(ssl: {alpn_protocols: %w[http/1.1]})
|
46
|
+
.with(
|
47
|
+
timeout: {
|
48
|
+
connect_timeout: 10,
|
49
|
+
read_timeout: 400,
|
50
|
+
keep_alive_timeout: 480,
|
51
|
+
request_timeout: 400,
|
52
|
+
operation_timeout: 400
|
53
|
+
}
|
54
|
+
)
|
45
55
|
end
|
46
56
|
|
47
57
|
def prepare_response(response)
|
48
|
-
return {
|
58
|
+
return {error: response.dig("error", "message")} if response["type"] == "error"
|
49
59
|
|
50
|
-
|
60
|
+
texts = []
|
61
|
+
tools = []
|
62
|
+
|
63
|
+
response["content"].each do |msg|
|
64
|
+
case msg["type"]
|
65
|
+
when "text"
|
66
|
+
texts << msg["text"].gsub(%r{\n?</?thinking>\n?}, "")
|
67
|
+
when "tool_use"
|
68
|
+
tools << msg.slice("name", "id", "input").transform_keys(&:to_sym)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
{text: texts.join("\n"), tools: tools}
|
51
73
|
end
|
52
74
|
end
|
53
75
|
end
|
data/lib/vercon/cli.rb
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "dry/cli"
|
4
4
|
|
5
|
-
require_relative
|
6
|
-
require_relative
|
5
|
+
require_relative "commands/init"
|
6
|
+
require_relative "commands/generate"
|
7
7
|
|
8
8
|
module Vercon
|
9
9
|
class CLI
|
10
10
|
extend Dry::CLI::Registry
|
11
11
|
|
12
|
-
register
|
13
|
-
register
|
12
|
+
register "init", Commands::Init, aliases: ["i"]
|
13
|
+
register "generate", Commands::Generate, aliases: ["g"]
|
14
14
|
end
|
15
15
|
end
|
@@ -1,28 +1,33 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
7
|
-
require
|
3
|
+
require "prism"
|
4
|
+
require "dry/files"
|
5
|
+
require "tty-spinner"
|
6
|
+
require "rouge"
|
7
|
+
require "tty-editor"
|
8
8
|
|
9
9
|
module Vercon
|
10
10
|
module Commands
|
11
11
|
class Generate < Dry::CLI::Command
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
12
|
+
AUTOFIXERS = {
|
13
|
+
standard: "bundle exec standardrb --fix %{file} > /dev/null 2>&1",
|
14
|
+
rubocop: "bundle exec rubocop -a %{file} > /dev/null 2>&1"
|
15
|
+
}
|
16
|
+
|
17
|
+
desc "Generate test file"
|
18
|
+
|
19
|
+
argument :path, desc: "Path to the ruby file"
|
20
|
+
|
21
|
+
option :edit_prompt, type: :boolean, default: false, aliases: ["e"],
|
22
|
+
desc: "Edit prompt before submitting to claude"
|
23
|
+
option :output_path, type: :string, default: nil, aliases: ["o"],
|
24
|
+
desc: "Path to save test file"
|
25
|
+
option :stdout, type: :boolean, default: false, aliases: ["s"],
|
26
|
+
desc: "Output test file to stdout instead of writing to test file"
|
27
|
+
option :force, type: :boolean, default: false, aliases: ["f"],
|
28
|
+
desc: "Force overwrite of existing test file"
|
29
|
+
option :open, type: :boolean, default: nil, aliases: ["p"],
|
30
|
+
desc: "Open test file in editor after generation"
|
26
31
|
|
27
32
|
def initialize
|
28
33
|
@config = Vercon::Config.new
|
@@ -41,10 +46,15 @@ module Vercon
|
|
41
46
|
current_test = files.exist?(output_path) ? files.read(output_path) : nil
|
42
47
|
|
43
48
|
result = generate_test_file(path, opts, current_test)
|
49
|
+
return if result.nil?
|
50
|
+
|
51
|
+
result = run_autofixes(result)
|
44
52
|
|
45
53
|
if opts[:stdout]
|
46
|
-
|
47
|
-
|
54
|
+
formatter = Rouge::Formatters::Terminal256.new(Rouge::Themes::Base16::Monokai.new)
|
55
|
+
lexer = Rouge::Lexers::Ruby.new
|
56
|
+
|
57
|
+
stdout.puts(formatter.format(lexer.lex(result)))
|
48
58
|
return
|
49
59
|
end
|
50
60
|
|
@@ -54,13 +64,11 @@ module Vercon
|
|
54
64
|
|
55
65
|
files.write(output_path, result)
|
56
66
|
|
57
|
-
run_rubocop(output_path) if include_gem?('rubocop') || include_gem?('standard')
|
58
|
-
|
59
67
|
stdout.ok("Test file saved at \"#{output_path}\" 🥳")
|
60
68
|
|
61
|
-
|
62
|
-
|
63
|
-
|
69
|
+
if opts[:open] == true || (opts[:open].nil? && config.open_by_default?)
|
70
|
+
TTY::Editor.new(raise_on_failure: true).open(output_path)
|
71
|
+
end
|
64
72
|
end
|
65
73
|
|
66
74
|
private
|
@@ -69,29 +77,29 @@ module Vercon
|
|
69
77
|
|
70
78
|
def can_generate?(path, _opts)
|
71
79
|
unless config.exists?
|
72
|
-
stdout.error(
|
80
|
+
stdout.error("Config file does not exist. Run `vercon init` to create a config file.")
|
73
81
|
return false
|
74
82
|
end
|
75
83
|
|
76
84
|
if path.nil? || path.empty?
|
77
|
-
stdout.error(
|
85
|
+
stdout.error("Path to ruby file is blank.")
|
78
86
|
return false
|
79
87
|
end
|
80
88
|
|
81
89
|
unless files.exist?(path)
|
82
|
-
stdout.error(
|
90
|
+
stdout.error("Ruby file does not exist.")
|
83
91
|
return false
|
84
92
|
end
|
85
93
|
|
86
94
|
expanded_path = files.expand_path(path)
|
87
95
|
|
88
96
|
if Prism.parse_file_failure?(expanded_path)
|
89
|
-
stdout.error(
|
97
|
+
stdout.error("Looks like the ruby file has syntax errors. Fix them before generating tests.")
|
90
98
|
return false
|
91
99
|
end
|
92
100
|
|
93
|
-
unless include_gem?(
|
94
|
-
stdout.error(
|
101
|
+
unless include_gem?("rspec")
|
102
|
+
stdout.error("RSpec is not installed. Vercon requires RSpec to generate test files.")
|
95
103
|
return false
|
96
104
|
end
|
97
105
|
|
@@ -99,15 +107,11 @@ module Vercon
|
|
99
107
|
end
|
100
108
|
|
101
109
|
def generate_test_file_path(path, _opts)
|
102
|
-
|
103
|
-
spinner = TTY::Spinner.new(
|
110
|
+
prompt = Vercon::Prompt.for_test_path(path: path)
|
111
|
+
spinner = TTY::Spinner.new("[:spinner] Preparing spec file path...", format: :flip)
|
104
112
|
spinner.auto_spin
|
105
113
|
|
106
|
-
result = Vercon::Claude.new.submit(
|
107
|
-
model: config.class::LOWEST_CLAUDE_MODEL,
|
108
|
-
system: system, user: user,
|
109
|
-
stop_sequences: [stop_sequence]
|
110
|
-
)
|
114
|
+
result = Vercon::Claude.new.submit(**prompt.merge(model: config.class::LOWEST_CLAUDE_MODEL))
|
111
115
|
spinner.stop
|
112
116
|
stdout.erase(lines: 1)
|
113
117
|
|
@@ -116,28 +120,28 @@ module Vercon
|
|
116
120
|
return
|
117
121
|
end
|
118
122
|
|
119
|
-
path = result[:text].match(/RSPEC FILE PATH: "(.+)"/)
|
123
|
+
path, = result[:text].match(/RSPEC FILE PATH: "(.+)"/).captures
|
120
124
|
|
121
125
|
if stdout.no?("Corresponding test file path should be \"#{path}\". Correct?")
|
122
|
-
path = stdout.ask(
|
126
|
+
path = stdout.ask("Enter a relative path of corresponding test:")
|
123
127
|
end
|
124
128
|
|
125
129
|
path
|
126
130
|
end
|
127
131
|
|
128
132
|
def generate_test_file(path, opts, current_test)
|
129
|
-
factories = Vercon::Factories.new.load if include_gem?(
|
130
|
-
|
133
|
+
factories = Vercon::Factories.new.load if include_gem?("factory_bot")
|
134
|
+
prompt = Vercon::Prompt.for_test_generation(
|
131
135
|
path: path, source: files.read(path),
|
132
136
|
factories: factories, current_test: current_test
|
133
137
|
)
|
134
|
-
|
135
|
-
return if system.nil? || user.nil?
|
138
|
+
prompt = ask_for_edits(**prompt) if opts[:edit_prompt]
|
139
|
+
return if prompt[:system].nil? || prompt[:user].nil? || prompt[:tools].nil?
|
136
140
|
|
137
|
-
spinner = TTY::Spinner.new(
|
141
|
+
spinner = TTY::Spinner.new("[:spinner] Generating spec file...", format: :flip)
|
138
142
|
spinner.auto_spin
|
139
143
|
|
140
|
-
result = Vercon::Claude.new.submit(
|
144
|
+
result = Vercon::Claude.new.submit(**prompt)
|
141
145
|
spinner.stop
|
142
146
|
stdout.erase(lines: 1)
|
143
147
|
|
@@ -146,49 +150,85 @@ module Vercon
|
|
146
150
|
return
|
147
151
|
end
|
148
152
|
|
149
|
-
result[:
|
153
|
+
tool = result[:tools].find { |tool| tool[:name] == "write_test_file" }
|
154
|
+
|
155
|
+
if tool.nil?
|
156
|
+
stdout.error('Claude did not return the "write_test_file" tool. Aborting generation.')
|
157
|
+
return nil
|
158
|
+
end
|
159
|
+
|
160
|
+
source = tool.dig(:input, "source_code")
|
161
|
+
source = source.match(/```ruby\n(.+)\n```/m).captures.first if source.include?("```ruby")
|
162
|
+
|
163
|
+
source
|
150
164
|
end
|
151
165
|
|
152
|
-
def
|
153
|
-
|
154
|
-
|
166
|
+
def run_autofixes(source)
|
167
|
+
file = Tempfile.new("source_spec.rb")
|
168
|
+
file.write(source)
|
169
|
+
file.open
|
155
170
|
|
156
|
-
|
171
|
+
AUTOFIXERS.each do |name, command|
|
172
|
+
next unless include_gem?(name.to_s)
|
157
173
|
|
158
|
-
|
159
|
-
|
174
|
+
spinner = TTY::Spinner.new("[:spinner] Running #{name}...", format: :flip)
|
175
|
+
spinner.auto_spin
|
176
|
+
|
177
|
+
system(format(command, file: file.path))
|
178
|
+
|
179
|
+
spinner.stop
|
180
|
+
stdout.erase(lines: 1)
|
181
|
+
end
|
182
|
+
|
183
|
+
file.read
|
184
|
+
ensure
|
185
|
+
file.unlink
|
160
186
|
end
|
161
187
|
|
162
|
-
def ask_for_edits(system
|
163
|
-
path =
|
164
|
-
text =
|
165
|
-
|
188
|
+
def ask_for_edits(system:, user:, tools: nil)
|
189
|
+
path = "~/.vercon_prompt.txt"
|
190
|
+
text = []
|
191
|
+
text << <<~EOF.strip
|
192
|
+
Please, do not remove magick comments like <System prompt> and others :)
|
193
|
+
#{tools.nil? ? "" : "When chaning tools, make sure to keep the general schema section intact, change descriptions only."}
|
194
|
+
|
166
195
|
<System prompt>
|
167
196
|
#{system}
|
197
|
+
|
168
198
|
<User prompt>
|
169
199
|
#{user}
|
170
200
|
EOF
|
201
|
+
text << <<~EOF.strip if tools
|
202
|
+
<Tools>
|
203
|
+
#{JSON.pretty_generate(tools)}
|
204
|
+
EOF
|
171
205
|
|
172
|
-
files.write(path, text)
|
206
|
+
files.write(path, text.join("\n\n"))
|
173
207
|
|
174
208
|
TTY::Editor.new(raise_on_failure: true).open(path)
|
175
|
-
TTY::Spinner.new(
|
209
|
+
TTY::Spinner.new("[:spinner] Waiting for changes...", format: :flip).run { sleep(rand(1..3)) }
|
176
210
|
stdout.erase(lines: 1)
|
177
211
|
|
178
|
-
if stdout.no?(
|
179
|
-
stdout.error(
|
180
|
-
return
|
212
|
+
if stdout.no?("Can we proceed?")
|
213
|
+
stdout.error("Generation aborted!")
|
214
|
+
return {}
|
181
215
|
end
|
182
216
|
|
183
|
-
|
217
|
+
result = files.read(path)
|
218
|
+
system, user = result.match(/<System prompt>(.+)\n<User prompt>(.+)/m).captures
|
219
|
+
if tools
|
220
|
+
user = user.match(/^(.+)\n\n<Tools>/m).captures.first
|
221
|
+
tools = result.match(/<Tools>(.+)/m)&.captures&.first
|
222
|
+
tools = JSON.parse(tools)
|
223
|
+
end
|
184
224
|
|
185
|
-
|
225
|
+
{system: system, user: user, tools: tools}.reject { |_, v| v.nil? }
|
186
226
|
ensure
|
187
227
|
files.delete(path)
|
188
228
|
end
|
189
229
|
|
190
230
|
def include_gem?(name)
|
191
|
-
files.read(
|
231
|
+
files.read("Gemfile").include?(name)
|
192
232
|
end
|
193
233
|
end
|
194
234
|
end
|
data/lib/vercon/commands/init.rb
CHANGED
@@ -3,10 +3,11 @@
|
|
3
3
|
module Vercon
|
4
4
|
module Commands
|
5
5
|
class Init < Dry::CLI::Command
|
6
|
-
desc
|
6
|
+
desc "Initialize vercon config"
|
7
7
|
|
8
|
-
option :
|
9
|
-
option :claude_model, desc:
|
8
|
+
option :claude_token, desc: "Claude API token"
|
9
|
+
option :claude_model, desc: "Claude model to use by default"
|
10
|
+
option :open, type: :boolean, default: nil, desc: "Open generated test file by default"
|
10
11
|
|
11
12
|
def initialize
|
12
13
|
@stdout = Vercon::Stdout.new
|
@@ -18,26 +19,23 @@ module Vercon
|
|
18
19
|
end
|
19
20
|
|
20
21
|
def call(**opts)
|
21
|
-
|
22
|
-
|
22
|
+
setup_token(opts)
|
23
|
+
setup_claude_model(opts)
|
24
|
+
setup_default_open(opts)
|
23
25
|
|
24
|
-
|
25
|
-
@stdout.ok("Config file #{@config_existed ? 'updated' : 'created'}!")
|
26
|
-
else
|
27
|
-
@stdout.warn('Config file is not touched.')
|
28
|
-
end
|
26
|
+
@stdout.ok("Config file #{@config_existed ? "updated" : "created"}!")
|
29
27
|
end
|
30
28
|
|
31
29
|
private
|
32
30
|
|
33
31
|
def setup_token(opts)
|
34
|
-
if @config.
|
32
|
+
if @config.claude_token && @stdout.no?("Claude API token already set to `#{@config.claude_token}`. Do you want to replace it?")
|
35
33
|
return
|
36
34
|
end
|
37
35
|
|
38
|
-
token = opts[:
|
39
|
-
token ||= @stdout.ask(
|
40
|
-
@config.
|
36
|
+
token = opts[:claude_token]
|
37
|
+
token ||= @stdout.ask("Provide your Claude API token:")
|
38
|
+
@config.claude_token = token
|
41
39
|
end
|
42
40
|
|
43
41
|
def setup_claude_model(opts)
|
@@ -46,10 +44,24 @@ module Vercon
|
|
46
44
|
end
|
47
45
|
|
48
46
|
model = opts[:claude_model]
|
49
|
-
model ||= @stdout.select(
|
50
|
-
|
47
|
+
model ||= @stdout.select("Select Claude model that will be used by default:", Vercon::Config::CLAUDE_MODELS,
|
48
|
+
default: Vercon::Config::DEFAULT_CLAUDE_MODEL, cycle: true)
|
51
49
|
@config.claude_model = model
|
52
50
|
end
|
51
|
+
|
52
|
+
def setup_default_open(opts)
|
53
|
+
open = opts[:open]
|
54
|
+
if open.nil?
|
55
|
+
open = @stdout.select(
|
56
|
+
"Open generated test file by default?",
|
57
|
+
{Yes: true, No: false},
|
58
|
+
default: "No",
|
59
|
+
cycle: true
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
@config.open_by_default = open
|
64
|
+
end
|
53
65
|
end
|
54
66
|
end
|
55
67
|
end
|
data/lib/vercon/config.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "yaml"
|
4
|
+
require "dry/files"
|
5
5
|
|
6
6
|
module Vercon
|
7
7
|
class Config
|
@@ -10,9 +10,9 @@ module Vercon
|
|
10
10
|
claude-3-sonnet-20240229
|
11
11
|
claude-3-opus-20240229
|
12
12
|
].freeze
|
13
|
-
DEFAULT_CLAUDE_MODEL =
|
14
|
-
LOWEST_CLAUDE_MODEL =
|
15
|
-
PATH =
|
13
|
+
DEFAULT_CLAUDE_MODEL = "claude-3-sonnet-20240229"
|
14
|
+
LOWEST_CLAUDE_MODEL = "claude-3-haiku-20240307"
|
15
|
+
PATH = "~/.vercon.yml"
|
16
16
|
|
17
17
|
def initialize
|
18
18
|
@files = Dry::Files.new
|
@@ -25,21 +25,28 @@ module Vercon
|
|
25
25
|
!@config.empty?
|
26
26
|
end
|
27
27
|
|
28
|
-
def
|
29
|
-
@config[
|
28
|
+
def claude_token
|
29
|
+
@config["claude_token"]
|
30
30
|
end
|
31
31
|
|
32
|
-
def
|
33
|
-
@config[
|
34
|
-
@files.write(@files.expand_path(PATH), YAML.safe_dump(@config))
|
32
|
+
def claude_model
|
33
|
+
@config["claude_model"]
|
35
34
|
end
|
36
35
|
|
37
|
-
def
|
38
|
-
@config[
|
36
|
+
def open_by_default?
|
37
|
+
@config["open_by_default"].nil? ? false : @config["open_by_default"]
|
39
38
|
end
|
40
39
|
|
41
|
-
|
42
|
-
|
40
|
+
%i[claude_token claude_model open_by_default].each do |method|
|
41
|
+
define_method(:"#{method}=") do |value|
|
42
|
+
@config[method.to_s] = value
|
43
|
+
write_config
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def write_config
|
43
50
|
@files.write(@files.expand_path(PATH), YAML.safe_dump(@config))
|
44
51
|
end
|
45
52
|
end
|
data/lib/vercon/factories.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "dry/files"
|
4
|
+
require "prism"
|
5
5
|
|
6
6
|
module Vercon
|
7
7
|
class Factories
|
8
|
-
PATH =
|
8
|
+
PATH = "./spec/factories"
|
9
9
|
|
10
10
|
def initialize
|
11
11
|
@files = Dry::Files.new
|
@@ -14,7 +14,7 @@ module Vercon
|
|
14
14
|
def load
|
15
15
|
return unless @files.directory?(PATH)
|
16
16
|
|
17
|
-
Dir[@files.expand_path(@files.join(PATH,
|
17
|
+
Dir[@files.expand_path(@files.join(PATH, "**", "*.rb"))].map do |file_path|
|
18
18
|
load_factory(file_path)
|
19
19
|
end.flatten.compact
|
20
20
|
end
|
data/lib/vercon/prompt.rb
CHANGED
@@ -2,11 +2,11 @@
|
|
2
2
|
|
3
3
|
module Vercon
|
4
4
|
class Prompt
|
5
|
-
|
6
|
-
END_SEQUENCE = '<END RESULT>'
|
5
|
+
END_SEQUENCE = "<END RESULT>"
|
7
6
|
|
7
|
+
class << self
|
8
8
|
def for_test_path(path:)
|
9
|
-
|
9
|
+
system = <<~PROMPT.strip
|
10
10
|
You are tasked as a professional Ruby and Ruby on Rails developer specialising in writing comprehensive RSpec unit tests for a Ruby class. You will receive a path to ruby file. Your objective is to generate a path corresponding RSpec unit test file. Make sure to use common practices used in Ruby on Rails community for structuring test file paths.
|
11
11
|
|
12
12
|
Provide a in the following format:
|
@@ -17,15 +17,15 @@ module Vercon
|
|
17
17
|
Make sure to include "#{END_SEQUENCE}" at the end of your test source code. It's required.
|
18
18
|
PROMPT
|
19
19
|
|
20
|
-
|
20
|
+
user = <<~PROMPT.strip
|
21
21
|
PATH: #{path.inspect}
|
22
22
|
PROMPT
|
23
23
|
|
24
|
-
|
24
|
+
{system: system, user: user, stop_sequences: [END_SEQUENCE]}
|
25
25
|
end
|
26
26
|
|
27
27
|
def for_test_generation(path:, source:, factories: nil, current_test: nil)
|
28
|
-
|
28
|
+
system = <<~PROMPT.strip
|
29
29
|
You are a professional Ruby developer specializing in writing comprehensive RSpec unit tests for a Ruby class. Your objective is to ensure each public method within the class is thoroughly tested. Below are the specifics you must adhere to:
|
30
30
|
|
31
31
|
- Structure:
|
@@ -59,25 +59,18 @@ module Vercon
|
|
59
59
|
- This includes testing with empty or nil values, large or small input values, and any other relevant scenarios specific to the class.
|
60
60
|
- Think critically about potential edge cases and ensure they are adequately covered in the tests.
|
61
61
|
|
62
|
-
Provide the result in the following format:
|
63
|
-
|
64
|
-
TEST SOURCE CODE:
|
65
|
-
```ruby
|
66
|
-
<the RSpec tests, without any additional comments or markdown instructions>
|
67
|
-
```
|
68
|
-
#{END_SEQUENCE}
|
69
|
-
|
70
|
-
Make sure to include "#{END_SEQUENCE}" at the end of your test source code.
|
71
|
-
|
72
62
|
If the user provides "CURRENT RSPEC FILE", use it as a base for your tests, improve it, and extend it according to the Ruby file being tested. Ensure that the existing tests are updated to meet the specified requirements and that new tests are added to achieve comprehensive coverage of the class's public methods.
|
73
63
|
|
74
64
|
Remember to focus on testing the behavior of the class rather than its implementation details. Aim for concise, readable, and maintainable tests that provide confidence in the correctness of the class. Use descriptive test names, keep tests isolated from each other, and follow RSpec best practices and conventions throughout your test suite.
|
65
|
+
|
66
|
+
After you have created the RSpec tests for the Ruby class, make sure to call "write_test_file" tool with the test source code.
|
67
|
+
You shouldn't comment your thinking process.
|
75
68
|
PROMPT
|
76
69
|
|
77
|
-
|
70
|
+
user = ["PATH: #{path.inspect}"]
|
78
71
|
|
79
72
|
if factories
|
80
|
-
|
73
|
+
user << <<~PROMPT.strip
|
81
74
|
AVAILABLE FACTORIES:
|
82
75
|
```json
|
83
76
|
#{JSON.dump(factories)}
|
@@ -85,7 +78,7 @@ module Vercon
|
|
85
78
|
PROMPT
|
86
79
|
end
|
87
80
|
|
88
|
-
|
81
|
+
user << <<~PROMPT.strip
|
89
82
|
CODE:
|
90
83
|
```ruby
|
91
84
|
#{source}
|
@@ -93,7 +86,7 @@ module Vercon
|
|
93
86
|
PROMPT
|
94
87
|
|
95
88
|
if current_test
|
96
|
-
|
89
|
+
user << <<~PROMPT.strip
|
97
90
|
CURRENT RSPEC FILE:
|
98
91
|
```ruby
|
99
92
|
#{current_test}
|
@@ -101,7 +94,24 @@ module Vercon
|
|
101
94
|
PROMPT
|
102
95
|
end
|
103
96
|
|
104
|
-
|
97
|
+
tools = [
|
98
|
+
{
|
99
|
+
name: "write_test_file",
|
100
|
+
description: "Tool to write the test file to disk",
|
101
|
+
input_schema: {
|
102
|
+
type: "object",
|
103
|
+
properties: {
|
104
|
+
source_code: {
|
105
|
+
type: "string",
|
106
|
+
description: "The source code of the test file to be written to disk. Make sure to include the complete test file content. Do not include any additional information, comments or markup."
|
107
|
+
}
|
108
|
+
},
|
109
|
+
required: ["source_code"]
|
110
|
+
}
|
111
|
+
}
|
112
|
+
]
|
113
|
+
|
114
|
+
{system: system, user: user.join("\n"), tools: tools}
|
105
115
|
end
|
106
116
|
end
|
107
117
|
end
|
data/lib/vercon/stdout.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "tty-prompt"
|
4
4
|
|
5
5
|
module Vercon
|
6
6
|
class Stdout
|
@@ -12,7 +12,7 @@ module Vercon
|
|
12
12
|
|
13
13
|
def write(message)
|
14
14
|
@stdout.puts(message)
|
15
|
-
@lines += 1
|
15
|
+
@lines += message.count("\n") + 1
|
16
16
|
end
|
17
17
|
|
18
18
|
def erase(lines: nil)
|
@@ -23,16 +23,16 @@ module Vercon
|
|
23
23
|
end
|
24
24
|
|
25
25
|
%i[ask yes? no? say ok warn error mask select].each do |method|
|
26
|
-
define_method(method) do |*args, &block|
|
27
|
-
@lines += 1
|
28
|
-
@prompt.send(method, *args, &block)
|
26
|
+
define_method(method) do |*args, **params, &block|
|
27
|
+
@lines += args.first.count("\n") + 1
|
28
|
+
@prompt.send(method, *args, **params, &block)
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
32
|
%i[puts print].each do |method|
|
33
|
-
define_method(method) do |*args, &block|
|
34
|
-
@lines += 1
|
35
|
-
@stdout.send(method, *args, &block)
|
33
|
+
define_method(method) do |*args, **params, &block|
|
34
|
+
@lines += args.first.count("\n") + 1
|
35
|
+
@stdout.send(method, *args, **params, &block)
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
data/lib/vercon/version.rb
CHANGED
data/lib/vercon.rb
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
3
|
+
require_relative "vercon/version"
|
4
|
+
require_relative "vercon/config"
|
5
|
+
require_relative "vercon/stdout"
|
6
|
+
require_relative "vercon/cli"
|
7
|
+
require_relative "vercon/claude"
|
8
|
+
require_relative "vercon/prompt"
|
9
|
+
require_relative "vercon/factories"
|
10
10
|
|
11
11
|
module Vercon
|
12
12
|
class Error < StandardError; end
|
data/vercon.gemspec
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "lib/vercon/version"
|
4
4
|
|
5
5
|
Gem::Specification.new do |spec|
|
6
|
-
spec.name =
|
6
|
+
spec.name = "vercon"
|
7
7
|
spec.version = Vercon::VERSION
|
8
|
-
spec.authors = [
|
9
|
-
spec.email = [
|
8
|
+
spec.authors = ["Alex Beznos"]
|
9
|
+
spec.email = ["beznosa@yahoo.com"]
|
10
10
|
|
11
|
-
spec.summary =
|
12
|
-
spec.description =
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
15
|
-
spec.required_ruby_version =
|
16
|
-
spec.required_rubygems_version =
|
11
|
+
spec.summary = "CLI tool to generate test files with Cloude 3"
|
12
|
+
spec.description = "CLI tool to generate test files with Cloude 3."
|
13
|
+
spec.homepage = "https://github.com/AlexBeznoss/vercon"
|
14
|
+
spec.license = "MIT"
|
15
|
+
spec.required_ruby_version = ">= 3.0"
|
16
|
+
spec.required_rubygems_version = ">= 3.3.11"
|
17
17
|
|
18
|
-
spec.metadata[
|
19
|
-
spec.metadata[
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
20
20
|
|
21
21
|
# Specify which files should be added to the gem when it is released.
|
22
22
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
@@ -26,18 +26,18 @@ Gem::Specification.new do |spec|
|
|
26
26
|
f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
|
27
27
|
end
|
28
28
|
end
|
29
|
-
spec.bindir =
|
29
|
+
spec.bindir = "exe"
|
30
30
|
spec.executables = [spec.name]
|
31
|
-
spec.require_paths = [
|
31
|
+
spec.require_paths = ["lib"]
|
32
32
|
|
33
|
-
spec.add_dependency
|
34
|
-
spec.add_dependency
|
35
|
-
spec.add_dependency
|
36
|
-
spec.add_dependency
|
37
|
-
spec.add_dependency
|
38
|
-
spec.add_dependency
|
39
|
-
spec.add_dependency
|
40
|
-
spec.add_dependency
|
33
|
+
spec.add_dependency "dry-cli", "~> 1.0", "< 2"
|
34
|
+
spec.add_dependency "dry-files", "~> 1.0", "< 2"
|
35
|
+
spec.add_dependency "httpx", "~> 1.2.3"
|
36
|
+
spec.add_dependency "prism", "~> 0.24"
|
37
|
+
spec.add_dependency "rouge", "~> 4.2.1"
|
38
|
+
spec.add_dependency "tty-editor", "~> 0.7.0"
|
39
|
+
spec.add_dependency "tty-prompt", "~> 0.23.1"
|
40
|
+
spec.add_dependency "tty-spinner", "~> 0.9.3"
|
41
41
|
|
42
42
|
# For more information and examples about making a new gem, check out our
|
43
43
|
# guide at: https://bundler.io/guides/creating_gem.html
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vercon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Beznos
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-04-
|
11
|
+
date: 2024-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dry-cli
|
@@ -79,33 +79,33 @@ dependencies:
|
|
79
79
|
- !ruby/object:Gem::Version
|
80
80
|
version: '0.24'
|
81
81
|
- !ruby/object:Gem::Dependency
|
82
|
-
name:
|
82
|
+
name: rouge
|
83
83
|
requirement: !ruby/object:Gem::Requirement
|
84
84
|
requirements:
|
85
85
|
- - "~>"
|
86
86
|
- !ruby/object:Gem::Version
|
87
|
-
version:
|
87
|
+
version: 4.2.1
|
88
88
|
type: :runtime
|
89
89
|
prerelease: false
|
90
90
|
version_requirements: !ruby/object:Gem::Requirement
|
91
91
|
requirements:
|
92
92
|
- - "~>"
|
93
93
|
- !ruby/object:Gem::Version
|
94
|
-
version:
|
94
|
+
version: 4.2.1
|
95
95
|
- !ruby/object:Gem::Dependency
|
96
|
-
name: tty-
|
96
|
+
name: tty-editor
|
97
97
|
requirement: !ruby/object:Gem::Requirement
|
98
98
|
requirements:
|
99
99
|
- - "~>"
|
100
100
|
- !ruby/object:Gem::Version
|
101
|
-
version:
|
101
|
+
version: 0.7.0
|
102
102
|
type: :runtime
|
103
103
|
prerelease: false
|
104
104
|
version_requirements: !ruby/object:Gem::Requirement
|
105
105
|
requirements:
|
106
106
|
- - "~>"
|
107
107
|
- !ruby/object:Gem::Version
|
108
|
-
version:
|
108
|
+
version: 0.7.0
|
109
109
|
- !ruby/object:Gem::Dependency
|
110
110
|
name: tty-prompt
|
111
111
|
requirement: !ruby/object:Gem::Requirement
|
@@ -143,6 +143,7 @@ extensions: []
|
|
143
143
|
extra_rdoc_files: []
|
144
144
|
files:
|
145
145
|
- ".rspec"
|
146
|
+
- ".simplecov"
|
146
147
|
- ".standard.yml"
|
147
148
|
- ".tool-versions"
|
148
149
|
- LICENSE.txt
|