tng 0.3.4 → 0.3.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e936ced36e475ba1ae147f9209794b592c639b96eb5573fff15e47175f5729d2
4
- data.tar.gz: bce3240ea48c7ef6cfbe2629296eaa28165f251625541151d2a9a93673eb5659
3
+ metadata.gz: 72069bd609868c0afa45280862175fd654e60e14087df5358c436f97437fd292
4
+ data.tar.gz: e15543832d8ecd552f73293c7d5cd9609c26546b0a7bc26c1302acedec13809b
5
5
  SHA512:
6
- metadata.gz: 7cf2e03008535e3d68dc928c699195aadd750b52c08a01bcf99037c565246c902d57b2cc6aaa7699e41811f86afa4e5ead4b33ce2441320cb7772e09838ad251
7
- data.tar.gz: 033bed50161aae9f0daf04c608ce9e56023fa3f46cbd30b520ce39307efdf498afc811e55f6564777c22a18092950613fb104e7dd944cfe1cdd2aa4bd4c8e41f
6
+ metadata.gz: d6b5cf2b393181cb1041ce06bb5fb3f662e4b0b1b8958697c9675ed9524367ce79d63bd8264333b3b290466ec427c5d7e2166ecb89316a9d65ef237fae120d0d
7
+ data.tar.gz: ea5c64cfe99afb371819970b4df8f35b88b144ad4f23808cbcdf92b15f3959937ba92d781751330c09bbd10d35eb784cfdf07e9ffbbe1b16071fbab9d6f0ab82
data/bin/tng CHANGED
@@ -340,7 +340,8 @@ class CLI
340
340
  progress.update("Extracting method context...", 60)
341
341
  progress.update("Generating method test code...", 80)
342
342
 
343
- result = Tng::Services::TestGenerator.new(@http_client).run_for_controller_method(controller, method_info, progress: progress)
343
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_controller_method(controller, method_info,
344
+ progress: progress)
344
345
 
345
346
  progress.update("Writing test file...", 100)
346
347
 
@@ -396,7 +397,8 @@ class CLI
396
397
  progress.update("Analyzing method dependencies...", 50)
397
398
  progress.update("Generating method test code...", 75)
398
399
 
399
- result = Tng::Services::TestGenerator.new(@http_client).run_for_model_method(model, method_info, progress: progress)
400
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_model_method(model, method_info,
401
+ progress: progress)
400
402
 
401
403
  progress.update("Writing test file...", 100)
402
404
 
@@ -416,7 +418,8 @@ class CLI
416
418
  progress.update("Analyzing method dependencies...", 50)
417
419
  progress.update("Generating method test code...", 75)
418
420
 
419
- result = Tng::Services::TestGenerator.new(@http_client).run_for_service_method(service, method_info, progress: progress)
421
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_service_method(service, method_info,
422
+ progress: progress)
420
423
 
421
424
  progress.update("Writing test file...", 100)
422
425
 
@@ -478,7 +481,8 @@ class CLI
478
481
  progress.update("Analyzing method dependencies...", 50)
479
482
  progress.update("Generating method test code...", 75)
480
483
 
481
- result = Tng::Services::TestGenerator.new(@http_client).run_for_other_method(other_file, method_info, progress: progress)
484
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_other_method(other_file, method_info,
485
+ progress: progress)
482
486
 
483
487
  progress.update("Writing test file...", 100)
484
488
 
@@ -653,8 +657,6 @@ class CLI
653
657
  end
654
658
  end
655
659
 
656
-
657
-
658
660
  def copy_to_clipboard(command)
659
661
  if RUBY_PLATFORM.include?("darwin")
660
662
  IO.popen("pbcopy", "w") { |io| io.print command }
@@ -671,8 +673,6 @@ class CLI
671
673
  end
672
674
  end
673
675
 
674
-
675
-
676
676
  def initialize_config_and_clients
677
677
  @config_initialized = true
678
678
 
Binary file
Binary file
Binary file
Binary file
@@ -1,399 +1,74 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "rails/generators"
4
- require "yaml"
5
- require "tng"
6
- require "pathname"
7
4
 
8
5
  module Tng
9
6
  module Generators
10
7
  class InstallGenerator < Rails::Generators::Base
11
- desc "Creates a tng.sh Ruby configuration file with comprehensive authentication and authorization support"
12
-
13
- def self.source_root
14
- @source_root ||= File.expand_path("templates", __dir__)
15
- end
8
+ desc "Creates a TNG configuration file"
16
9
 
17
10
  def create_tng_configuration
18
- @test_framework = detect_test_framework
19
- @authentication_library = detect_authentication_library
20
- @authz_library = detect_authorization_library
21
- @factory_library = detect_factory_library
22
- @test_examples = detect_test_examples
23
-
24
11
  initializer_path = "config/initializers/tng.rb"
25
12
 
26
13
  if File.exist?(initializer_path)
27
14
  say "Configuration file already exists at #{initializer_path}", :yellow
28
15
  if yes?("Do you want to overwrite it? (y/n)")
29
16
  remove_file initializer_path
30
- create_file initializer_path, ruby_configuration_template
17
+ create_file initializer_path, configuration_template
31
18
  say "TNG configuration file updated!", :green
32
19
  else
33
20
  say "Skipping configuration file creation.", :blue
34
- nil
35
21
  end
36
22
  else
37
- create_file initializer_path, ruby_configuration_template
38
- say "tng.sh configuration file created at #{initializer_path}", :green
23
+ create_file initializer_path, configuration_template
24
+ say "TNG configuration file created at #{initializer_path}", :green
39
25
  end
40
26
  end
41
27
 
42
- def ruby_configuration_template
43
- framework_config = generate_framework_config(@test_framework)
44
-
45
- if @authentication_library && @authentication_library != "none"
46
- auth_enabled = "config.authentication_enabled = true"
47
- auth_comment = " (detected: #{@authentication_library})"
48
- auth_lib = "config.authentication_library = \"#{@authentication_library}\""
49
- else
50
- auth_enabled = "config.authentication_enabled = true"
51
- auth_comment = ""
52
- auth_lib = "config.authentication_library = nil"
53
- end
54
-
55
- if @authz_library && @authz_library != "none"
56
- authz_lib = "config.authorization_library = \"#{@authz_library}\""
57
- authz_comment = " (detected: #{@authz_library})"
58
- else
59
- authz_lib = "config.authorization_library = nil"
60
- authz_comment = ""
61
- end
62
-
63
- factory_lib = "config.factory_library = \"#{@factory_library}\""
64
-
65
- framework_specific = generate_framework_specific_config(@test_framework, framework_config)
66
-
67
- test_examples_config = @test_examples.any? ? @test_examples.inspect : "[]"
68
- [
69
- "# frozen_string_literal: true",
70
- "",
71
- "return unless Rails.env.development?",
72
- "",
73
- "Tng.configure do |config|",
74
- " config.api_key = nil",
75
- " # You dont need to change this url, unless you will instructed by the CLI.",
76
- " config.base_url = \"https://app.tng.sh/\"",
77
- "",
78
- " # Testing Framework",
79
- " config.testing_framework = \"#{@test_framework}\" # Options: minitest, rspec",
80
- framework_specific,
81
- " config.mock_library = \"#{framework_config["mock_library"]}\" # Options: mocha, minitest/mock, rspec-mocks, nil",
82
- " config.http_mock_library = \"#{framework_config["http_mock_library"]}\" # Options: webmock, vcr, httparty, nil",
83
- " #{factory_lib} # Options: factory_bot, factory_girl, fabrication, fabricator, fixtures, active_record",
84
- "",
85
- " # Test Examples",
86
- " # Example test files for LLM to learn patterns and reduce hallucinations",
87
- " # Format: [{{\"name\" => \"test_name\", \"path\" => \"spec/models/user_spec.rb\"}}]",
88
- " # Leave empty [] to auto-detect from project",
89
- " config.test_examples = #{test_examples_config}",
90
- "",
91
- " # Source Code Reading Configuration",
92
- " # When enabled (true), TNG will only read the file where the method is located",
93
- " # and will not analyze other files in the project. This may increase the accuracy of the tests,",
94
- " # but it may also increase the time it takes to generate the tests.",
95
- " config.read_file_source_code = false # Options: true, false",
96
- "",
97
- " # Authentication#{auth_comment}",
98
- " #{auth_enabled} # Options: true, false",
99
- " #{auth_lib} # Options: devise, clearance, sorcery, nil",
100
- "",
101
- "",
102
- " # ⚠️ IMPORTANT: AUTHENTICATION CONFIGURATION REQUIRED ⚠️",
103
- " # You MUST configure your authentication methods below for TNG to work properly.",
104
- " # Uncomment and modify the authentication_methods configuration:",
105
- "",
106
- " # Authentication Methods (multiple methods supported)",
107
- " # Supported authentication types: session, devise, jwt, token_auth, basic_auth, oauth, headers, custom, nil",
108
- " # EXAMPLE: Uncomment and modify these examples to match your app's authentication:",
109
- "",
110
- " # config.authentication_methods = [",
111
- " # {",
112
- " # method: \"authenticate_user_via_session!\",",
113
- " # file_location: \"app/controllers/application_controller.rb\",",
114
- " # auth_type: \"session\"",
115
- " # },",
116
- " # {",
117
- " # method: \"authenticate_user_via_api_key!\",",
118
- " # file_location: \"app/controllers/application_controller.rb\",",
119
- " # auth_type: \"headers\"",
120
- " # }",
121
- " # ]",
122
- " # ⚠️ Remember to configure your authentication methods above! ⚠️",
123
- "",
124
- " # Authorization#{authz_comment}",
125
- " #{authz_lib} # Options: cancancan, pundit, rolify, nil",
126
- "end"
127
- ].join("\n") + "\n"
128
- end
129
-
130
28
  private
131
29
 
132
- def detect_test_framework
133
- rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
134
-
135
- return "rspec" if File.exist?(File.join(rails_root, ".rspec")) ||
136
- File.exist?(File.join(rails_root, "spec", "spec_helper.rb")) ||
137
- Dir.exist?(File.join(rails_root, "spec"))
138
-
139
- return "minitest" if Dir.exist?(File.join(rails_root, "test")) ||
140
- File.exist?(File.join(rails_root, "test", "test_helper.rb"))
141
-
142
- begin
143
- if defined?(Bundler)
144
- gemfile_specs = Bundler.load.specs
145
- return "rspec" if gemfile_specs.any? { |spec| spec.name == "rspec-rails" }
146
- return "minitest" if gemfile_specs.any? { |spec| spec.name == "minitest" }
147
- end
148
- rescue StandardError
149
- # Fallback if Bundler isn't available
150
- end
151
-
152
- "minitest" # Default fallback
153
- end
154
-
155
- def detect_authentication_library
156
- return unless defined?(Bundler)
157
-
158
- begin
159
- gemfile_specs = Bundler.load.specs
160
- return "devise" if gemfile_specs.any? { |spec| spec.name == "devise" }
161
- return "clearance" if gemfile_specs.any? { |spec| spec.name == "clearance" }
162
- return "sorcery" if gemfile_specs.any? { |spec| spec.name == "sorcery" }
163
- rescue StandardError
164
- # Fallback if Bundler isn't available
165
- nil
166
- end
167
-
168
- nil
169
- end
170
-
171
- def detect_authorization_library
172
- return unless defined?(Bundler)
173
-
174
- gemfile_specs = Bundler.load.specs
175
- return "cancancan" if gemfile_specs.any? { |spec| spec.name == "cancancan" }
176
- return "pundit" if gemfile_specs.any? { |spec| spec.name == "pundit" }
177
- return "rolify" if gemfile_specs.any? { |spec| spec.name == "rolify" }
178
-
179
- nil
180
- end
181
-
182
- def detect_factory_library
183
- return "active_record" unless defined?(Bundler)
184
-
185
- rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
186
-
187
- # Check for fixtures in test/fixtures or spec/fixtures
188
- test_fixtures_path = File.join(rails_root, "test", "fixtures")
189
- spec_fixtures_path = File.join(rails_root, "spec", "fixtures")
190
-
191
- return "fixtures" if Dir.exist?(test_fixtures_path) && !Dir.empty?(test_fixtures_path)
192
-
193
- return "fixtures" if Dir.exist?(spec_fixtures_path) && !Dir.empty?(spec_fixtures_path)
194
-
195
- gemfile_specs = Bundler.load.specs
196
- return "factory_bot" if gemfile_specs.any? { |spec| spec.name.match(/factory_bot/) }
197
- return "factory_girl" if gemfile_specs.any? { |spec| spec.name.match(/factory_girl/) }
198
- return "fabrication" if gemfile_specs.any? { |spec| spec.name.match(/fabrication/) }
199
- return "fabricator" if gemfile_specs.any? { |spec| spec.name.match(/fabricator/) }
200
-
201
- "active_record"
202
- rescue StandardError
203
- # Fallback if Bundler isn't available
204
- "active_record"
205
- end
206
-
207
- def detect_test_examples
208
- rails_root = defined?(Rails) && Rails.root ? Rails.root.to_s : Dir.pwd
209
- examples = []
210
-
211
- # Determine test directory based on framework
212
- test_dir = @test_framework == "rspec" ? "spec" : "test"
213
-
214
- # Comprehensive exclusion list for build artifacts, caches, dependencies
215
- exclude_dirs = [
216
- ".git", "log", "tmp", "vendor", "node_modules",
217
- "coverage", "public", "storage", "db", "config",
218
- "lib/tasks", "bin", ".bundle", ".vscode", ".idea"
219
- ]
220
-
221
- # Also exclude files larger than 500KB (probably not good examples)
222
- max_file_size = 500 * 1024 # 500KB
223
-
224
- # Find all test directories and subdirectories
225
- test_directories = find_test_directories(rails_root, test_dir, exclude_dirs)
226
-
227
- # Determine collection strategy based on folder structure
228
- main_test_dir = File.join(rails_root, test_dir)
229
- has_subfolders = test_directories.size > 1
230
-
231
- if has_subfolders
232
- # If we have subfolders, take 2 files per directory
233
- collected_files = []
234
- test_directories.each do |dir_path|
235
- dir_files = find_valid_test_files_in_directory(dir_path, max_file_size)
236
- # Take up to 2 files from this directory
237
- collected_files.concat(dir_files.first(2))
30
+ def configuration_template
31
+ <<~RUBY
32
+ # frozen_string_literal: true
33
+
34
+ return unless Rails.env.development?
35
+
36
+ Tng.configure do |config|
37
+ config.api_key = ENV["TNG_API_KEY"]
38
+ config.base_url = ENV["API_BASE_URI"] || "https://app.tng.sh/"
39
+
40
+ # Test Helper File (optional - auto-detected if not set)
41
+ # Uncomment to override auto-detection with a custom path
42
+ # Default auto-detection checks: spec/rails_helper.rb, spec/spec_helper.rb, test/test_helper.rb
43
+ # config.test_helper_path = "spec/rails_helper.rb"
44
+
45
+ # Authentication Configuration
46
+ # Set to false if your application does not require authentication
47
+ # config.authentication_enabled = true
48
+
49
+ # ⚠️ IMPORTANT: AUTHENTICATION CONFIGURATION REQUIRED ⚠️
50
+ # You MUST configure your authentication methods below for TNG to work properly.
51
+ # Uncomment and modify the authentication_methods configuration:
52
+
53
+ # Authentication Methods (multiple methods supported)
54
+ # Supported authentication types: session, devise, jwt, token_auth, basic_auth, oauth, headers, custom, nil
55
+ # EXAMPLE: Uncomment and modify these examples to match your app's authentication:
56
+
57
+ # config.authentication_methods = [
58
+ # {
59
+ # method: "authenticate_user_via_session!",
60
+ # file_location: "app/controllers/application_controller.rb",
61
+ # auth_type: "session"
62
+ # },
63
+ # {
64
+ # method: "authenticate_user_via_api_key!",
65
+ # file_location: "app/controllers/application_controller.rb",
66
+ # auth_type: "headers"
67
+ # }
68
+ # ]
69
+ # ⚠️ Remember to configure your authentication methods above! ⚠️
238
70
  end
239
- else
240
- # If no subfolders, take up to 5 files from main test directory
241
- collected_files = find_valid_test_files_in_directory(main_test_dir, max_file_size).first(5)
242
- end
243
-
244
- # Sort all collected files by modification time (recent first), then by size (smaller files first)
245
- collected_files.sort_by! { |f| [-File.mtime(f).to_i, File.size(f)] }
246
-
247
- # Convert to examples format
248
- collected_files.first(5).each do |test_file|
249
- examples << {
250
- "name" => File.basename(test_file),
251
- "path" => Pathname.new(test_file).relative_path_from(Pathname.new(rails_root)).to_s
252
- }
253
- rescue StandardError
254
- next
255
- end
256
-
257
- examples
258
- rescue StandardError
259
- # Return empty array if anything goes wrong
260
- []
261
- end
262
-
263
- def find_test_directories(rails_root, test_dir, exclude_dirs)
264
- directories = []
265
- main_test_path = File.join(rails_root, test_dir)
266
-
267
- return [] unless Dir.exist?(main_test_path)
268
-
269
- # Add main test directory
270
- directories << main_test_path
271
-
272
- # Find all subdirectories recursively
273
- Dir.glob(File.join(main_test_path, "**/*/")).each do |dir|
274
- # Skip excluded directories
275
- next if exclude_dirs.any? { |excluded| dir.include?("/#{excluded}/") }
276
-
277
- # Skip if it's not a directory with actual test files
278
- next unless test_files?(dir)
279
-
280
- directories << dir
281
- end
282
-
283
- directories.uniq
284
- end
285
-
286
- def test_files?(dir_path)
287
- return false unless Dir.exist?(dir_path)
288
-
289
- pattern = @test_framework == "rspec" ? "*_spec.rb" : "*_test.rb"
290
- Dir.glob(File.join(dir_path, pattern)).any?
291
- end
292
-
293
- def find_valid_test_files_in_directory(dir_path, max_file_size)
294
- return [] unless Dir.exist?(dir_path)
295
-
296
- pattern = @test_framework == "rspec" ? "*_spec.rb" : "*_test.rb"
297
- files = []
298
-
299
- Dir.glob(File.join(dir_path, pattern)).each do |test_file|
300
- next if File.size(test_file) > max_file_size
301
- next unless valid_test_file?(test_file)
302
-
303
- files << test_file
304
- rescue StandardError
305
- next
306
- end
307
-
308
- files
309
- end
310
-
311
- def valid_test_file?(file_path)
312
- return false unless File.exist?(file_path) && File.readable?(file_path)
313
- return false if File.size(file_path) == 0
314
-
315
- begin
316
- # Read first 1KB of the file
317
- content = File.read(file_path, 1024)
318
-
319
- # Check for Ruby test indicators based on framework
320
- test_indicators = if @test_framework == "rspec"
321
- [
322
- "describe ", # RSpec describe blocks
323
- "context ", # RSpec context blocks
324
- "it ", # RSpec examples
325
- "specify ", # RSpec specify
326
- "expect(", # RSpec expectations
327
- "before(", # RSpec hooks
328
- "let(", # RSpec let
329
- "subject ", # RSpec subject
330
- 'require "spec_helper"', # RSpec spec helper
331
- 'require "rails_helper"' # RSpec rails helper
332
- ]
333
- else # minitest
334
- [
335
- "def test_", # Minitest test methods
336
- "class.*Test", # Test classes
337
- "assert ", # Assertions
338
- "refute ", # Refutations
339
- 'require "test_helper"', # Minitest helper
340
- 'require "minitest"', # Minitest require
341
- "MiniTest::Test" # Minitest base class
342
- ]
343
- end
344
-
345
- # Must contain at least one test indicator
346
- return false unless test_indicators.any? { |indicator| content.include?(indicator) }
347
-
348
- # Basic Ruby syntax check - should not have obvious non-Ruby content
349
- return false if content.scan(/#!/).size > 5 || content.include?("<?xml")
350
-
351
- true
352
- rescue StandardError
353
- false
354
- end
355
- end
356
-
357
- def generate_framework_config(framework)
358
- if framework == "minitest"
359
- {
360
- "test_style" => "test_block", # Options: spec, unit, test_block
361
- "setup_style" => true, # Options: true, false
362
- "assertion_style" => "assert/refute", # Options: assert/refute, assert/assert_not, must/wont
363
- "teardown_style" => false, # Options: true, false
364
- "http_mock_library" => "webmock", # Options: webmock, vcr, httparty, none
365
- "mock_library" => "minitest/mock" # Options: mocha, minitest/mock, rspec-mocks, none
366
- }
367
- else # rspec
368
- {
369
- "describe_style" => true, # Options: true, false
370
- "context_style" => "context", # Options: context, describe
371
- "it_style" => "it", # Options: it, specify
372
- "before_style" => "before", # Options: before, setup
373
- "after_style" => "after", # Options: after, teardown
374
- "let_style" => true, # Options: true, false
375
- "subject_style" => true, # Options: true, false
376
- "mock_library" => "rspec-mocks", # Options: rspec-mocks, mocha, flexmock, none
377
- "http_mock_library" => "webmock" # Options: webmock, vcr, httparty, none
378
- }
379
- end
380
- end
381
-
382
- def generate_framework_specific_config(framework, framework_config)
383
- if framework == "minitest"
384
- " config.test_style = \"#{framework_config["test_style"]}\" # Options: spec, unit, test_block\n" +
385
- " config.setup_style = #{framework_config["setup_style"]} # Options: true, false\n" +
386
- " config.assertion_style = \"#{framework_config["assertion_style"]}\" # Options: assert/refute, assert/assert_not, must/wont\n" +
387
- " config.teardown_style = #{framework_config["teardown_style"]} # Options: true, false"
388
- else # rspec
389
- " config.describe_style = #{framework_config["describe_style"]} # Options: true, false\n" +
390
- " config.context_style = \"#{framework_config["context_style"]}\" # Options: context, describe\n" +
391
- " config.it_style = \"#{framework_config["it_style"]}\" # Options: it, specify\n" +
392
- " config.before_style = \"#{framework_config["before_style"]}\" # Options: before, setup\n" +
393
- " config.after_style = \"#{framework_config["after_style"]}\" # Options: after, teardown\n" +
394
- " config.let_style = #{framework_config["let_style"]} # Options: true, false\n" +
395
- " config.subject_style = #{framework_config["subject_style"]} # Options: true, false"
396
- end
71
+ RUBY
397
72
  end
398
73
  end
399
74
  end
@@ -36,7 +36,6 @@ module Tng
36
36
  generate_test_for_type(other_file, method_info, :other, progress: progress)
37
37
  end
38
38
 
39
-
40
39
  def generate_test_for_type(file_object, method_info, type, progress: nil)
41
40
  start_time = Time.now
42
41
 
@@ -50,14 +49,11 @@ module Tng
50
49
  job_data = JSON.parse(response.body)
51
50
 
52
51
  # Check for authentication errors
53
- if response.status == 401
54
- return { error: :auth_failed, message: "Invalid or missing API key" }
55
- end
52
+ return { error: :auth_failed, message: "Invalid or missing API key" } if response.status == 401
56
53
 
57
54
  # Check for forbidden responses (usage limits or invalid keys)
58
- if response.status == 403
59
- return { error: :auth_failed, message: "API key expired or usage limit reached" }
60
- end
55
+ return { error: :auth_failed, message: "API key expired or usage limit reached" } if response.status == 403
56
+
61
57
  job_id = job_data["job_id"]
62
58
 
63
59
  return unless job_id
@@ -114,15 +110,15 @@ module Tng
114
110
  # We create 4 generic steps for the agents
115
111
  # Note: indices are relative to the current session, so we just append them.
116
112
  # But we need their indices to update them later.
117
- # Since we can't ask progress for current index easily without hacking,
113
+ # Since we can't ask progress for current index easily without hacking,
118
114
  # we rely on the fact that we call update 4 times.
119
-
115
+
120
116
  # Use a base offset if we could know it, but we can't reliably.
121
117
  # Actually, if we use explicit_step, we need absolute indices.
122
118
  # Let's assume the previous steps were 0, 1, 2, 3 based on bin/tng.
123
119
  # So we start at 4.
124
- base_idx = 4
125
-
120
+ base_idx = 4
121
+
126
122
  progress.update("Context Builder: Pending...", nil, step_increment: true)
127
123
  agent_step_indices["context_agent_status"] = base_idx
128
124
 
@@ -146,10 +142,10 @@ module Tng
146
142
 
147
143
  # Calculate pseudo-percentage
148
144
  if progress
149
- pct = ((attempts.to_f / max_attempts.to_f) * 95.0).to_i
150
- # Update global progress bar without adding a new step (explicit_step=4 updates Context Builder line, but we want to just update percentage?)
151
- # The progress bar component takes the percent from the *last* update message.
152
- # So we can pass specific percent when we update any agent step.
145
+ pct = ((attempts.to_f / max_attempts.to_f) * 95.0).to_i
146
+ # Update global progress bar without adding a new step (explicit_step=4 updates Context Builder line, but we want to just update percentage?)
147
+ # The progress bar component takes the percent from the *last* update message.
148
+ # So we can pass specific percent when we update any agent step.
153
149
  else
154
150
  pct = 0
155
151
  end
@@ -166,32 +162,32 @@ module Tng
166
162
  begin
167
163
  status_data = JSON.parse(status_response.body.to_s)
168
164
  status = status_data["status"]
169
-
165
+
170
166
  # Update UI with granular info
171
167
  if progress && status_data["info"].is_a?(Hash)
172
168
  info = status_data["info"]
173
-
169
+
174
170
  agent_step_indices.each do |key, step_idx|
175
- item_data = info[key]
176
- next unless item_data
177
-
178
- agent_status = "pending"
179
- values = []
180
-
181
- if item_data.is_a?(Hash)
182
- agent_status = item_data["status"] || "pending"
183
- values = item_data["values"] || []
184
- else
185
- agent_status = item_data.to_s
186
- end
187
-
188
- label = case key
189
- when "context_agent_status" then "Context Builder"
190
- when "style_agent_status" then "Style Analyzer"
191
- when "logical_issue_status" then "Logic Analyzer"
192
- when "behavior_expert_status" then "Logic Generator"
193
- else key
194
- end
171
+ item_data = info[key]
172
+ next unless item_data
173
+
174
+ agent_status = "pending"
175
+ values = []
176
+
177
+ if item_data.is_a?(Hash)
178
+ agent_status = item_data["status"] || "pending"
179
+ values = item_data["values"] || []
180
+ else
181
+ agent_status = item_data.to_s
182
+ end
183
+
184
+ label = case key
185
+ when "context_agent_status" then "Context Builder"
186
+ when "style_agent_status" then "Style Analyzer"
187
+ when "logical_issue_status" then "Logic Analyzer"
188
+ when "behavior_expert_status" then "Logic Generator"
189
+ else key
190
+ end
195
191
 
196
192
  msg = if agent_status == "processing"
197
193
  "#{label}: Processing..."
@@ -213,8 +209,8 @@ module Tng
213
209
 
214
210
  # Pass percentage only on the last step (Logic Generator) to keep main bar moving?
215
211
  # Or pass it on all updates.
216
- p = (step_idx == agent_step_indices["behavior_expert_status"]) ? pct : nil
217
-
212
+ p = step_idx == agent_step_indices["behavior_expert_status"] ? pct : nil
213
+
218
214
  progress.update(msg, p, step_increment: false, explicit_step: step_idx)
219
215
  end
220
216
  end
@@ -222,7 +218,7 @@ module Tng
222
218
  case status
223
219
  when "completed"
224
220
  debug_log("Test generation completed!") if debug_enabled?
225
- return status_data["result"]
221
+ return status_data["result"].merge("analysis" => status_data["analysis"])
226
222
  when "failed"
227
223
  debug_log("Test generation failed: #{status_data["error"]}") if debug_enabled?
228
224
  return { error: :generation_failed, message: status_data["error"] }
@@ -35,17 +35,6 @@ module Tng
35
35
  end
36
36
  Tng.config[:authentication_entry_points_with_source] = auth_entry_points_with_source
37
37
 
38
- # Add source content to test examples for API requests
39
- if Tng.config[:test_examples]&.any?
40
- Tng.config[:test_examples] = Tng.config[:test_examples].map do |example|
41
- next example unless example.is_a?(Hash) && example["path"]
42
-
43
- example.merge("source" => read_test_file_content(example["path"]))
44
- rescue StandardError
45
- example
46
- end
47
- end
48
-
49
38
  Tng.config
50
39
  end
51
40
 
@@ -381,8 +381,10 @@ module Tng
381
381
  def update(message, percent = nil, step_increment: true, explicit_step: nil)
382
382
  step_idx = if explicit_step
383
383
  explicit_step
384
+ elsif step_increment
385
+ @step
384
386
  else
385
- step_increment ? @step : (@step > 0 ? @step - 1 : 0)
387
+ (@step > 0 ? @step - 1 : 0)
386
388
  end
387
389
 
388
390
  data = { type: "step", step: step_idx, message: message }
@@ -403,4 +405,3 @@ module Tng
403
405
  end
404
406
  end
405
407
  end
406
-
data/lib/tng/utils.rb CHANGED
@@ -80,9 +80,7 @@ module Tng
80
80
  puts "📋 Raw API response: #{test_content[0..200]}..." if ENV["DEBUG"]
81
81
  parsed_response = JSON.parse(test_content)
82
82
 
83
- if parsed_response["error"]
84
- return { error: :generation_failed, message: parsed_response["error"] }
85
- end
83
+ return { error: :generation_failed, message: parsed_response["error"] } if parsed_response["error"]
86
84
  # Validate required fields
87
85
  unless parsed_response["file_content"]
88
86
  return { error: :invalid_response, message: "API response missing file_content" }
@@ -90,16 +88,16 @@ module Tng
90
88
 
91
89
  # Handle both possible field names for file path
92
90
  file_path = parsed_response["test_file_path"] || parsed_response["file_path"] || parsed_response["file_name"] || parsed_response["file"]
93
- unless file_path
94
- return { error: :invalid_response, message: "API response missing file path" }
95
- end
91
+ return { error: :invalid_response, message: "API response missing file path" } unless file_path
96
92
 
97
93
  begin
98
- File.write(file_path, parsed_response["file_content"])
94
+ clean_content = cleanup_file_content(parsed_response["file_content"])
95
+ File.write(file_path, clean_content)
99
96
  rescue Errno::ENOENT
100
97
  # Create directory if it doesn't exist
101
98
  FileUtils.mkdir_p(File.dirname(file_path))
102
- File.write(file_path, parsed_response["file_content"])
99
+ clean_content = cleanup_file_content(parsed_response["file_content"])
100
+ File.write(file_path, clean_content)
103
101
  end
104
102
  absolute_path = File.expand_path(file_path)
105
103
 
@@ -123,22 +121,47 @@ module Tng
123
121
  }
124
122
  end
125
123
 
126
- def self.fixture_content
127
- factory_library = Tng.factory_library || "active_record"
128
-
129
- case factory_library.downcase
130
- when "fixtures"
131
- load_all_fixtures_data
132
- when "fabrication", "fabricator"
133
- load_all_fabricator_data
134
- when "factory_girl", "factory_bot"
135
- load_all_factory_data
136
- when "active_record"
137
- load_all_active_record_data
138
- else
139
- puts "⚠️ Warning: Unknown factory library '#{factory_library}'. Using Active Record object creation as fallback."
140
- load_all_fixtures_data
124
+ def self.cleanup_file_content(content)
125
+ return content unless content.is_a?(String)
126
+
127
+ cleaned = content.dup
128
+
129
+ # Strip trailing whitespace first
130
+ cleaned.rstrip!
131
+
132
+ # Remove trailing JSON artifacts that shouldn't be in Ruby code
133
+ # Common patterns: ends with `end"` or `end"\n}` or just `}`
134
+ loop do
135
+ original = cleaned.dup
136
+
137
+ # Remove trailing lone } or " that shouldn't be there
138
+ cleaned.sub!(/\n\s*\}\s*\z/, "\n")
139
+ cleaned.sub!(/"\s*\z/, "")
140
+ cleaned.sub!(/\n\s*"\s*\z/, "\n")
141
+ cleaned.sub!(/end"\s*\z/, "end")
142
+
143
+ break if cleaned == original
141
144
  end
145
+
146
+ # Ensure file ends with single newline
147
+ cleaned.rstrip!
148
+ cleaned << "\n" unless cleaned.empty?
149
+
150
+ cleaned
151
+ end
152
+
153
+ def self.fixture_content
154
+ # Auto-detect: try fixtures first, then factory_bot, then fabricator
155
+ fixtures = load_all_fixtures_data
156
+ return fixtures if fixtures.any?
157
+
158
+ factories = load_all_factory_data
159
+ return factories if factories.any?
160
+
161
+ fabricators = load_all_fabricator_data
162
+ return fabricators if fabricators.any?
163
+
164
+ {}
142
165
  end
143
166
 
144
167
  def self.load_all_fixtures_data
data/lib/tng/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tng
4
- VERSION = "0.3.4"
4
+ VERSION = "0.3.6"
5
5
  end
data/lib/tng.rb CHANGED
@@ -66,16 +66,8 @@ module Tng
66
66
  @config = {
67
67
  api_key: nil,
68
68
  base_url: "https://app.tng.sh/",
69
- testing_framework: "minitest",
70
- authentication_enabled: false,
71
- authorization_library: nil,
72
- authentication_library: nil,
73
- authentication_methods: [],
74
- mock_library: "minitest/mock",
75
- http_mock_library: "webmock",
76
- factory_library: "active_record",
77
- test_examples: [],
78
- read_file_source_code: false
69
+ test_helper_path: nil,
70
+ authentication_methods: []
79
71
  }
80
72
 
81
73
  def self.configure
@@ -102,37 +94,12 @@ module Tng
102
94
  @config[:base_url]
103
95
  end
104
96
 
105
- def self.testing_framework=(value)
106
- @config[:testing_framework] = value
107
- initialize_framework_defaults(value)
97
+ def self.test_helper_path=(value)
98
+ @config[:test_helper_path] = value
108
99
  end
109
100
 
110
- def self.testing_framework
111
- @config[:testing_framework]
112
- end
113
-
114
- def self.authentication_enabled=(value)
115
- @config[:authentication_enabled] = value
116
- end
117
-
118
- def self.authentication_enabled
119
- @config[:authentication_enabled]
120
- end
121
-
122
- def self.authentication_library=(value)
123
- @config[:authentication_library] = value
124
- end
125
-
126
- def self.authentication_library
127
- @config[:authentication_library]
128
- end
129
-
130
- def self.authorization_library=(value)
131
- @config[:authorization_library] = value
132
- end
133
-
134
- def self.authorization_library
135
- @config[:authorization_library]
101
+ def self.test_helper_path
102
+ @config[:test_helper_path]
136
103
  end
137
104
 
138
105
  def self.authentication_methods=(value)
@@ -142,178 +109,4 @@ module Tng
142
109
  def self.authentication_methods
143
110
  @config[:authentication_methods]
144
111
  end
145
-
146
- def self.mock_library=(value)
147
- @config[:mock_library] = value
148
- end
149
-
150
- def self.mock_library
151
- @config[:mock_library]
152
- end
153
-
154
- def self.http_mock_library=(value)
155
- @config[:http_mock_library] = value
156
- end
157
-
158
- def self.http_mock_library
159
- @config[:http_mock_library]
160
- end
161
-
162
- def self.factory_library=(value)
163
- @config[:factory_library] = value
164
- end
165
-
166
- def self.factory_library
167
- @config[:factory_library]
168
- end
169
-
170
- def self.test_style=(value)
171
- @config[:test_style] = value
172
- end
173
-
174
- def self.test_style
175
- @config[:test_style]
176
- end
177
-
178
- def self.setup_style=(value)
179
- @config[:setup_style] = value
180
- end
181
-
182
- def self.setup_style
183
- @config[:setup_style]
184
- end
185
-
186
- def self.assertion_style=(value)
187
- @config[:assertion_style] = value
188
- end
189
-
190
- def self.assertion_style
191
- @config[:assertion_style]
192
- end
193
-
194
- def self.teardown_style=(value)
195
- @config[:teardown_style] = value
196
- end
197
-
198
- def self.teardown_style
199
- @config[:teardown_style]
200
- end
201
-
202
- def self.describe_style=(value)
203
- @config[:describe_style] = value
204
- end
205
-
206
- def self.describe_style
207
- @config[:describe_style]
208
- end
209
-
210
- def self.context_style=(value)
211
- @config[:context_style] = value
212
- end
213
-
214
- def self.context_style
215
- @config[:context_style]
216
- end
217
-
218
- def self.it_style=(value)
219
- @config[:it_style] = value
220
- end
221
-
222
- def self.it_style
223
- @config[:it_style]
224
- end
225
-
226
- def self.before_style=(value)
227
- @config[:before_style] = value
228
- end
229
-
230
- def self.before_style
231
- @config[:before_style]
232
- end
233
-
234
- def self.after_style=(value)
235
- @config[:after_style] = value
236
- end
237
-
238
- def self.after_style
239
- @config[:after_style]
240
- end
241
-
242
- def self.let_style=(value)
243
- @config[:let_style] = value
244
- end
245
-
246
- def self.let_style
247
- @config[:let_style]
248
- end
249
-
250
- def self.subject_style=(value)
251
- @config[:subject_style] = value
252
- end
253
-
254
- def self.subject_style
255
- @config[:subject_style]
256
- end
257
-
258
- def self.test_examples=(value)
259
- @config[:test_examples] = value
260
- end
261
-
262
- def self.test_examples
263
- @config[:test_examples]
264
- end
265
-
266
- def self.read_file_source_code=(value)
267
- @config[:read_file_source_code] = value
268
- end
269
-
270
- def self.read_file_source_code
271
- @config[:read_file_source_code]
272
- end
273
-
274
- def self.authentication_configured?
275
- return false unless authentication_enabled
276
- return false if authentication_methods.nil? || authentication_methods.empty?
277
-
278
- authentication_methods.all? do |method|
279
- method.is_a?(Hash) &&
280
- method.key?(:method) && !method[:method].to_s.strip.empty? &&
281
- method.key?(:file_location) && !method[:file_location].to_s.strip.empty? &&
282
- method.key?(:auth_type) && !method[:auth_type].to_s.strip.empty?
283
- end
284
- end
285
-
286
- def self.initialize_framework_defaults(framework)
287
- if framework == "minitest"
288
- @config.merge!({
289
- test_style: "spec",
290
- setup_style: false,
291
- assertion_style: "assert",
292
- teardown_style: false
293
- })
294
- @config.delete(:describe_style)
295
- @config.delete(:context_style)
296
- @config.delete(:it_style)
297
- @config.delete(:before_style)
298
- @config.delete(:after_style)
299
- @config.delete(:let_style)
300
- @config.delete(:subject_style)
301
- elsif framework == "rspec"
302
- @config.merge!({
303
- describe_style: true,
304
- context_style: "context",
305
- it_style: "it",
306
- before_style: "before",
307
- after_style: "after",
308
- let_style: true,
309
- subject_style: true
310
- })
311
- @config.delete(:test_style)
312
- @config.delete(:setup_style)
313
- @config.delete(:assertion_style)
314
- @config.delete(:teardown_style)
315
- end
316
- end
317
-
318
- initialize_framework_defaults(@config[:testing_framework])
319
112
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tng
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.4
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - ralucab
@@ -162,9 +162,6 @@ files:
162
162
  - binaries/go-ui-darwin-arm64
163
163
  - binaries/go-ui-linux-amd64
164
164
  - binaries/go-ui-linux-arm64
165
- - binaries/tng-darwin-arm64.bundle
166
- - binaries/tng-linux-arm64.so
167
- - binaries/tng-linux-x86_64.so
168
165
  - lib/generators/tng/install_generator.rb
169
166
  - lib/tng.rb
170
167
  - lib/tng/analyzers/controller.rb
@@ -217,7 +214,7 @@ post_install_message: "┌ TNG ────────────────
217
214
  \ │\n│ • bundle exec
218
215
  tng --help - Show help information │\n│ │\n│
219
216
  \ \U0001F4A1 Generate tests for individual methods with precision │\n└────────────────────────────────────────────────────────────
220
- v0.3.4 ┘\n"
217
+ v0.3.6 ┘\n"
221
218
  rdoc_options: []
222
219
  require_paths:
223
220
  - lib
Binary file
Binary file
Binary file