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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8472e1a2fafc85a47fa7fd66553ecb16dd051d867165ff3441b8a883cb62b007
4
- data.tar.gz: 9bbcc1a5af63dc57d0d74a0b7d2a089cc78ec4b814868b858f3ce6a059bb881e
3
+ metadata.gz: c277e56e0c21fbb3a590af48d47d65d020fc93df1e1b127ac4e90124426b6b18
4
+ data.tar.gz: 2a7e8ce4d06bc8ab9cb15708871107ab34249afab8f61709ab798343e0d85242
5
5
  SHA512:
6
- metadata.gz: bf335632885d7cee074bb756448c501a8b06cf31d8090d29827b98b836cf3b7ca644163302ef048161fbc48c226bd8643397f68ad3a5e87f82907015a5285c69
7
- data.tar.gz: abafa240143904a41c04b121b55b4fb16b2fb4d81d27ca3977733c7151f625b28febf50f4ff6604c2b263e66963a4e09207f8c842ba382378a439d3ca7d9307e
6
+ metadata.gz: b556db258d22cbd92eac0c94fe62c2cee5a7d4995dfe8a6790b97e6c514936cb0e9c4ef03f21f55c2d7b61fa830750c6d1045e3e487a39b0f5a959af8ec40632
7
+ data.tar.gz: e3dfc208e7bc28f4ba5f8717539f82f52751d086d4ec77c652f06f895d92109bb1a1ff3b2dbfe5a0eb26d0587f92885dcdf3071fcf67b75005cdcde14215786b
data/.simplecov ADDED
@@ -0,0 +1,3 @@
1
+ SimpleCov.start do
2
+ add_filter "spec"
3
+ end
data/README.md CHANGED
@@ -1,34 +1,44 @@
1
1
  # Vercon
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ Vercon - a handy little gem that takes the pain out of writing tests for your Ruby projects. 🔥
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/vercon`. To experiment with that code, run `bin/console` for an interactive prompt.
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
- ## Installation
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
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
9
+ Claude analyzes your code to understand how it works, then uses that knowledge to put together relevant tests.
10
10
 
11
- Install the gem and add to the application's Gemfile by executing:
11
+ Just let the AI handle the tedious test writing for you 🚀
12
12
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
13
+ Give Vercon a try and save yourself some time and headaches when it comes to testing your Ruby apps.
14
14
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
15
+ Easy, efficient, no fuss 👌
16
16
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
17
+ ## How to use
18
18
 
19
- ## Usage
19
+ Install the gem by executing:
20
20
 
21
- TODO: Write usage instructions here
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`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
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/[USERNAME]/vercon.
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 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require 'standard/rake'
8
+ require "standard/rake"
9
9
 
10
10
  task default: %i[spec standard]
data/exe/vercon CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  # require "vercon"
5
5
  # TODO: change back
6
- require_relative '../lib/vercon'
6
+ require_relative "../lib/vercon"
7
7
 
8
8
  cli = Dry::CLI.new(Vercon::CLI)
9
9
 
data/lib/vercon/claude.rb CHANGED
@@ -1,53 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'httpx'
3
+ require "httpx"
4
4
 
5
5
  module Vercon
6
6
  class Claude
7
- BASE_URL = 'https://api.anthropic.com'
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.token
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
- messages: nil)
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 || [{ role: 'user', content: user }]
25
- }.reject { |_, v| v.nil? || v == '' }
24
+ messages: messages || [{role: "user", content: user}],
25
+ tools: tools
26
+ }.reject { |_, v| v.nil? || ["", [], {}].include?(v) }
26
27
 
27
- client.post('/v1/messages', body: body.to_json).then { |res| prepare_response(res.json) }
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
- { 'x-api-key' => @api_token, 'anthropic-version' => '2023-06-01' }
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
- .plugin(:retries)
40
- .with(headers: { 'Content-Type' => 'application/json', 'Cache-Control' => 'no-cache' })
41
- .with(headers: extra_headers)
42
- .with(origin: BASE_URL)
43
- .with(ssl: { alpn_protocols: %w[http/1.1] })
44
- .with(timeout: { keep_alive_timeout: 180 })
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 { error: response.dig('error', 'message') } if response['type'] == 'error'
58
+ return {error: response.dig("error", "message")} if response["type"] == "error"
49
59
 
50
- { text: response.dig('content', 0, 'text') }
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 'dry/cli'
3
+ require "dry/cli"
4
4
 
5
- require_relative 'commands/init'
6
- require_relative 'commands/generate'
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 'init', Commands::Init, aliases: ['i']
13
- register 'generate', Commands::Generate, aliases: ['g']
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 'prism'
4
- require 'dry/files'
5
- require 'tty-spinner'
6
- require 'tty-pager'
7
- require 'tty-editor'
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
- desc 'Generate test file'
13
-
14
- argument :path, desc: 'Path to the ruby file'
15
-
16
- option :edit_prompt, type: :boolean, default: false, aliases: ['e'],
17
- desc: 'Edit prompt before submitting to claude'
18
- option :output_path, type: :string, default: nil, aliases: ['o'],
19
- desc: 'Path to save test file'
20
- option :stdout, type: :boolean, default: false, aliases: ['s'],
21
- desc: 'Output test file to stdout instead of writing to test file'
22
- option :force, type: :boolean, default: false, aliases: ['f'],
23
- desc: 'Force overwrite of existing test file'
24
- option :open, type: :boolean, default: false, aliases: ['p'],
25
- desc: 'Open test file in editor after generation'
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
- pager = TTY::Pager.new
47
- pager.page(result)
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
- return unless opts[:open]
62
-
63
- TTY::Editor.new(raise_on_failure: true).open(output_path)
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('Config file does not exist. Run `vercon init` to create a config file.')
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('Path to ruby file is blank.')
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('Ruby file does not exist.')
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('Looks like the ruby file has syntax errors. Fix them before generating tests.')
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?('rspec')
94
- stdout.error('RSpec is not installed. Vercon requires RSpec to generate test files.')
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
- system, user, stop_sequence = Vercon::Prompt.for_test_path(path: path)
103
- spinner = TTY::Spinner.new('[:spinner] Preparing spec file path...', format: :flip)
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: "(.+)"/)[1]
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('Enter a relative path of corresponding test:')
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?('factory_bot')
130
- system, user, stop_sequence = Vercon::Prompt.for_test_generation(
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
- system, user = ask_for_edits(system, user) if opts[:edit_prompt]
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('[:spinner] Generating spec file...', format: :flip)
141
+ spinner = TTY::Spinner.new("[:spinner] Generating spec file...", format: :flip)
138
142
  spinner.auto_spin
139
143
 
140
- result = Vercon::Claude.new.submit(system: system, user: user, stop_sequences: [stop_sequence])
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[:text].match(/TEST SOURCE CODE:\n```ruby\n(.+)\n```/m)[1]
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 run_rubocop(path)
153
- spinner = TTY::Spinner.new('[:spinner] Running RuboCop...', format: :flip)
154
- spinner.auto_spin
166
+ def run_autofixes(source)
167
+ file = Tempfile.new("source_spec.rb")
168
+ file.write(source)
169
+ file.open
155
170
 
156
- system("bundle exec rubocop -A #{files.expand_path(path)} > /dev/null 2>&1")
171
+ AUTOFIXERS.each do |name, command|
172
+ next unless include_gem?(name.to_s)
157
173
 
158
- spinner.stop
159
- stdout.erase(lines: 1)
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, user)
163
- path = '~/.vercon_prompt.txt'
164
- text = <<~EOF.strip
165
- Please, do not remove magick comments :)
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('[:spinner] Waiting for changes...', format: :flip).run { sleep(rand(1..3)) }
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?('Can we proceed?')
179
- stdout.error('Generation aborted!')
180
- return []
212
+ if stdout.no?("Can we proceed?")
213
+ stdout.error("Generation aborted!")
214
+ return {}
181
215
  end
182
216
 
183
- system, user = files.read(path).match(/<System prompt>\n(.+)\n<User prompt>\n(.+)/m).captures
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
- [system, user]
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('Gemfile').include?(name)
231
+ files.read("Gemfile").include?(name)
192
232
  end
193
233
  end
194
234
  end
@@ -3,10 +3,11 @@
3
3
  module Vercon
4
4
  module Commands
5
5
  class Init < Dry::CLI::Command
6
- desc 'Initialize vercon config'
6
+ desc "Initialize vercon config"
7
7
 
8
- option :token, desc: 'Claude API token'
9
- option :claude_model, desc: 'Claude model to use by default'
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
- token_changed = setup_token(opts)
22
- claude_changed = setup_claude_model(opts)
22
+ setup_token(opts)
23
+ setup_claude_model(opts)
24
+ setup_default_open(opts)
23
25
 
24
- if token_changed || claude_changed
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.token && @stdout.no?("Claude API token already set to `#{@config.token}`. Do you want to replace it?")
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[:token]
39
- token ||= @stdout.ask('Provide your Claude API token:')
40
- @config.token = token
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('Select Claude model that will be used by default:', Vercon::Config::CLAUDE_MODELS,
50
- default: Vercon::Config::DEFAULT_CLAUDE_MODEL, cycle: true)
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 'yaml'
4
- require 'dry/files'
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 = 'claude-3-sonnet-20240229'
14
- LOWEST_CLAUDE_MODEL = 'claude-3-haiku-20240307'
15
- PATH = '~/.vercon.yml'
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 token
29
- @config['claude_token']
28
+ def claude_token
29
+ @config["claude_token"]
30
30
  end
31
31
 
32
- def token=(value)
33
- @config['claude_token'] = value
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 claude_model
38
- @config['claude_model']
36
+ def open_by_default?
37
+ @config["open_by_default"].nil? ? false : @config["open_by_default"]
39
38
  end
40
39
 
41
- def claude_model=(value)
42
- @config['claude_model'] = value
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
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'dry/files'
4
- require 'prism'
3
+ require "dry/files"
4
+ require "prism"
5
5
 
6
6
  module Vercon
7
7
  class Factories
8
- PATH = './spec/factories'
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, '**', '*.rb'))].map do |file_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
- class << self
6
- END_SEQUENCE = '<END RESULT>'
5
+ END_SEQUENCE = "<END RESULT>"
7
6
 
7
+ class << self
8
8
  def for_test_path(path:)
9
- system_prompt = <<~PROMPT.strip
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
- user_prompt = <<~PROMPT.strip
20
+ user = <<~PROMPT.strip
21
21
  PATH: #{path.inspect}
22
22
  PROMPT
23
23
 
24
- [system_prompt, user_prompt, END_SEQUENCE]
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
- system_prompt = <<~PROMPT.strip
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
- user_prompt = ["PATH: #{path.inspect}"]
70
+ user = ["PATH: #{path.inspect}"]
78
71
 
79
72
  if factories
80
- user_prompt << <<~PROMPT.strip
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
- user_prompt << <<~PROMPT.strip
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
- user_prompt << <<~PROMPT.strip
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
- [system_prompt, user_prompt.join("\n"), END_SEQUENCE]
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 'tty-prompt'
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vercon
4
- VERSION = '0.0.1'
4
+ VERSION = "0.0.2"
5
5
  end
data/lib/vercon.rb CHANGED
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
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'
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 'lib/vercon/version'
3
+ require_relative "lib/vercon/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = 'vercon'
6
+ spec.name = "vercon"
7
7
  spec.version = Vercon::VERSION
8
- spec.authors = ['Alex Beznos']
9
- spec.email = ['beznosa@yahoo.com']
8
+ spec.authors = ["Alex Beznos"]
9
+ spec.email = ["beznosa@yahoo.com"]
10
10
 
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'
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['homepage_uri'] = spec.homepage
19
- spec.metadata['source_code_uri'] = spec.homepage
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 = 'exe'
29
+ spec.bindir = "exe"
30
30
  spec.executables = [spec.name]
31
- spec.require_paths = ['lib']
31
+ spec.require_paths = ["lib"]
32
32
 
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 'tty-editor', '~> 0.7.0'
38
- spec.add_dependency 'tty-pager', '~> 0.14'
39
- spec.add_dependency 'tty-prompt', '~> 0.23.1'
40
- spec.add_dependency 'tty-spinner', '~> 0.9.3'
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.1
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-15 00:00:00.000000000 Z
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: tty-editor
82
+ name: rouge
83
83
  requirement: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
- version: 0.7.0
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: 0.7.0
94
+ version: 4.2.1
95
95
  - !ruby/object:Gem::Dependency
96
- name: tty-pager
96
+ name: tty-editor
97
97
  requirement: !ruby/object:Gem::Requirement
98
98
  requirements:
99
99
  - - "~>"
100
100
  - !ruby/object:Gem::Version
101
- version: '0.14'
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: '0.14'
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