tng 0.3.8 → 0.3.9

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: cbf8e76f0412c426150b5a9cef4d3a077d9747bf02c16ea6086f621cb78236a0
4
- data.tar.gz: 5bb42890de6580034ffc78df9b699433667e07e7877c7dc280ca53d998fa29cf
3
+ metadata.gz: 8caa50f15bf264b0c8c1ec375469881d00c1137385f127c28c1e4a8a18bb9554
4
+ data.tar.gz: 56c8dde536a3b54f1e8d0020aae0093b9014b749af79511df5903f920f66c6dd
5
5
  SHA512:
6
- metadata.gz: 868acc7d7fa9b539fac0b9906662cdf906a115da97f86643f8d2c88f9015b536e3ec75bbb50537df0a2283232207d7531a12b81eb8af4bdc8a1aa4abb4a83122
7
- data.tar.gz: 1410ad3af1e8b0bfbfa7d9849ba293cddca599e4dfda976f6433651814343592e39ef40aec5ca7c884e4ad28f004ab86bd36a384e1c334ab008629bec9ea3bd5
6
+ metadata.gz: 185d1c9670f66fdd3eb2f469c5109bbdf04c8ca2157c9de3983cd332972dc30c8038926b95013a8a1a2bc6b21d12be9a3f8efb060787936a97202bc0d526d8a1
7
+ data.tar.gz: 4374f5134a64c7b3c5c3d0b4a9b4bd785c555b0932a7552d285b0aa74ebadd1745a7b0395dcaef257426cbbc3e5328d2ca026fa9650d78147cbd4d5c48971801
data/bin/tng CHANGED
@@ -10,6 +10,7 @@ require "tng"
10
10
  require "tng/services/direct_generation"
11
11
  require "tng/services/extract_methods"
12
12
  require "tng/services/file_type_detector"
13
+ require "tng/services/fix_orchestrator"
13
14
 
14
15
  require "tty-screen"
15
16
  require "tty-option"
@@ -57,6 +58,18 @@ class CLI
57
58
  desc "Method name (for per-method tests)"
58
59
  end
59
60
 
61
+ flag :audit do
62
+ short "-a"
63
+ long "--audit"
64
+ desc "Run in audit mode (find issues and behaviours instead of generating tests)"
65
+ end
66
+
67
+ flag :fix do
68
+ short "-x"
69
+ long "--fix"
70
+ desc "Run in fix mode (automatically repair syntax, lint, and test errors)"
71
+ end
72
+
60
73
  flag :help do
61
74
  short "-h"
62
75
  long "--help"
@@ -90,7 +103,9 @@ class CLI
90
103
 
91
104
  initialize_config_and_clients if rails_loaded
92
105
 
93
- if params[:file]
106
+ if params[:fix]
107
+ handle_fix_command
108
+ elsif params[:file]
94
109
  run_direct_generation
95
110
  else
96
111
  if find_rails_root && !rails_loaded && !defined?(Rails)
@@ -121,6 +136,10 @@ class CLI
121
136
  normalized << "--file=#{::Regexp.last_match(2)}"
122
137
  when /^(?:--)?(method|m)=(.+)$/
123
138
  normalized << "--method=#{::Regexp.last_match(2)}"
139
+ when /^(?:--)?(audit|a)$/
140
+ normalized << "--audit"
141
+ when /^(?:--)?(fix|x)$/
142
+ normalized << "--fix"
124
143
  when /^(help|h)=(.+)$/
125
144
  normalized << "--help=#{::Regexp.last_match(2)}"
126
145
  when /^--file$/, /^-f$/
@@ -151,8 +170,11 @@ class CLI
151
170
  arg = positional_args[0]
152
171
  has_file = normalized.any? { |a| a.match?(/^--file/) }
153
172
  has_method = normalized.any? { |a| a.match?(/^--method/) }
173
+ has_fix = normalized.any? { |a| a.match?(/^--fix/) }
154
174
 
155
- if !has_file && (arg.end_with?(".rb") || arg.include?("/"))
175
+ if has_fix && !has_file
176
+ normalized << "--file=#{arg}"
177
+ elsif !has_file && (arg.end_with?(".rb") || arg.include?("/"))
156
178
  normalized << "--file=#{arg}"
157
179
  elsif !has_method && !arg.include?("/")
158
180
  normalized << "--method=#{arg}"
@@ -171,6 +193,8 @@ class CLI
171
193
  case choice
172
194
  when "tests"
173
195
  handle_test_generation
196
+ when "audit"
197
+ handle_audit_method
174
198
  when "stats"
175
199
  user_stats
176
200
  when "about"
@@ -201,6 +225,332 @@ class CLI
201
225
  end
202
226
  end
203
227
 
228
+ def handle_audit_method
229
+ choice = @go_ui.show_test_type_menu("audit")
230
+
231
+ case choice
232
+ when "controller"
233
+ audit_controller_method
234
+ when "model"
235
+ audit_model_method
236
+ when "service"
237
+ audit_service_method
238
+ when "other"
239
+ audit_other_method
240
+ end
241
+ end
242
+
243
+ def audit_controller_method
244
+ controllers = nil
245
+
246
+ @go_ui.show_spinner("Analyzing controllers...") do
247
+ controllers = Tng::Analyzers::Controller.files_in_dir("app/controllers").map do |file|
248
+ relative_path = file[:path].gsub(%r{^.*app/controllers/}, "").gsub(".rb", "")
249
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
250
+ { name: namespaced_name, path: file[:path] }
251
+ end
252
+ { success: true, message: "Found #{controllers.length} controllers" }
253
+ end
254
+
255
+ if controllers.empty?
256
+ @go_ui.show_no_items("controllers")
257
+ return
258
+ end
259
+
260
+ items = controllers.map { |c| { name: c[:name], path: c[:path] } }
261
+
262
+ loop do
263
+ selected_name = @go_ui.show_list_view("Select Controller to Audit", items)
264
+ return if selected_name == "back"
265
+
266
+ controller_choice = controllers.find { |c| c[:name] == selected_name }
267
+ next unless controller_choice
268
+
269
+ show_controller_audit_method_selection(controller_choice)
270
+ # If we return here, it means user pressed back from method selection
271
+ # Loop will show controller list again
272
+ end
273
+ end
274
+
275
+ def show_controller_audit_method_selection(controller)
276
+ methods = extract_controller_methods(controller)
277
+
278
+ if methods.empty?
279
+ @go_ui.show_no_items("methods in #{controller[:name]}")
280
+ return
281
+ end
282
+
283
+ items = methods.map { |m| { name: m[:name], path: controller[:name] } }
284
+ selected_name = @go_ui.show_list_view("Select Method to Audit", items)
285
+ return if selected_name == "back"
286
+
287
+ method_choice = methods.find { |m| m[:name] == selected_name }
288
+ return unless method_choice
289
+
290
+ run_audit_for_controller_method(controller, method_choice)
291
+ end
292
+
293
+ def run_audit_for_controller_method(controller, method_info)
294
+ result = nil
295
+
296
+ @go_ui.show_progress("Auditing #{controller[:name]}##{method_info[:name]}") do |progress|
297
+ progress.update("Analyzing method context...", 25)
298
+ progress.update("Running logical analysis...", 50)
299
+ progress.update("Detecting issues and behaviours...", 75)
300
+
301
+ result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_controller_method(
302
+ controller, method_info, progress: progress
303
+ )
304
+
305
+ progress.update("Processing results...", 100)
306
+
307
+ if result&.dig(:error)
308
+ { message: result[:message] || "Audit failed", error: result[:error] }
309
+ else
310
+ { message: "Audit complete!" }
311
+ end
312
+ end
313
+
314
+ return @go_ui.show_auth_error(result[:message] || "Audit failed") if result&.dig(:error)
315
+
316
+ display_audit_results(result)
317
+ end
318
+
319
+ def display_audit_results(result)
320
+ audit_data = result[:audit_results]
321
+
322
+ # Save to JSON for persistence
323
+ File.write("audit.json", JSON.pretty_generate(audit_data))
324
+ puts @pastel.green("💾 Audit results saved to audit.json")
325
+
326
+ # Send entire audit_data to go-ui (includes issues, behaviours, method_name, class_name, method_source_with_lines)
327
+ @go_ui.show_audit_results(audit_data, "issues")
328
+ end
329
+
330
+ # Placeholder methods for other component types
331
+ def audit_model_method
332
+ models = nil
333
+
334
+ @go_ui.show_spinner("Analyzing models...") do
335
+ models = Tng::Analyzers::Model.files_in_dir("app/models").map do |file|
336
+ relative_path = file[:path].gsub(%r{^.*app/models/}, "").gsub(".rb", "")
337
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
338
+ { name: namespaced_name, path: file[:path] }
339
+ end
340
+ { success: true, message: "Found #{models.length} models" }
341
+ end
342
+
343
+ if models.empty?
344
+ @go_ui.show_no_items("models")
345
+ return
346
+ end
347
+
348
+ items = models.map { |m| { name: m[:name], path: m[:path] } }
349
+
350
+ loop do
351
+ selected_name = @go_ui.show_list_view("Select Model to Audit", items)
352
+ return if selected_name == "back"
353
+
354
+ model_choice = models.find { |m| m[:name] == selected_name }
355
+ next unless model_choice
356
+
357
+ show_model_audit_method_selection(model_choice)
358
+ end
359
+ end
360
+
361
+ def show_model_audit_method_selection(model)
362
+ methods = extract_model_methods(model)
363
+
364
+ if methods.empty?
365
+ @go_ui.show_no_items("methods in #{model[:name]}")
366
+ return
367
+ end
368
+
369
+ items = methods.map { |m| { name: m[:name], path: model[:name] } }
370
+ selected_name = @go_ui.show_list_view("Select Method to Audit", items)
371
+ return if selected_name == "back"
372
+
373
+ method_choice = methods.find { |m| m[:name] == selected_name }
374
+ return unless method_choice
375
+
376
+ run_audit_for_model_method(model, method_choice)
377
+ end
378
+
379
+ def run_audit_for_model_method(model, method_info)
380
+ result = nil
381
+
382
+ @go_ui.show_progress("Auditing #{model[:name]}##{method_info[:name]}") do |progress|
383
+ progress.update("Parsing method details...", 25)
384
+ progress.update("Analyzing method context...", 25)
385
+ progress.update("Running logical analysis...", 50)
386
+ progress.update("Detecting issues and behaviours...", 75)
387
+
388
+ result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_model_method(
389
+ model, method_info, progress: progress
390
+ )
391
+
392
+ progress.update("Processing results...", 100)
393
+
394
+ if result&.dig(:error)
395
+ { message: result[:message] || "Audit failed", error: result[:error] }
396
+ else
397
+ { message: "Audit complete!" }
398
+ end
399
+ end
400
+
401
+ return @go_ui.show_auth_error(result[:message] || "Audit failed") if result&.dig(:error)
402
+
403
+ display_audit_results(result)
404
+ end
405
+
406
+ def audit_service_method
407
+ services = nil
408
+
409
+ @go_ui.show_spinner("Analyzing services...") do
410
+ services = Tng::Analyzers::Service.files_in_dir.map do |file|
411
+ relative_path = file[:path].gsub(%r{^.*app/services?/}, "").gsub(".rb", "")
412
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
413
+ { name: namespaced_name, path: file[:path] }
414
+ end
415
+ { success: true, message: "Found #{services.length} services" }
416
+ end
417
+
418
+ if services.empty?
419
+ @go_ui.show_no_items("services")
420
+ return
421
+ end
422
+
423
+ items = services.map { |s| { name: s[:name], path: s[:path] } }
424
+
425
+ loop do
426
+ selected_name = @go_ui.show_list_view("Select Service to Audit", items)
427
+ return if selected_name == "back"
428
+
429
+ service_choice = services.find { |s| s[:name] == selected_name }
430
+ next unless service_choice
431
+
432
+ show_service_audit_method_selection(service_choice)
433
+ end
434
+ end
435
+
436
+ def show_service_audit_method_selection(service)
437
+ methods = extract_service_methods(service)
438
+
439
+ if methods.empty?
440
+ @go_ui.show_no_items("methods in #{service[:name]}")
441
+ return
442
+ end
443
+
444
+ items = methods.map { |m| { name: m[:name], path: service[:name] } }
445
+ selected_name = @go_ui.show_list_view("Select Method to Audit", items)
446
+ return if selected_name == "back"
447
+
448
+ method_choice = methods.find { |m| m[:name] == selected_name }
449
+ return unless method_choice
450
+
451
+ run_audit_for_service_method(service, method_choice)
452
+ end
453
+
454
+ def run_audit_for_service_method(service, method_info)
455
+ result = nil
456
+
457
+ @go_ui.show_progress("Auditing #{service[:name]}##{method_info[:name]}") do |progress|
458
+ progress.update("Parsing method details...", 25)
459
+ progress.update("Analyzing method context...", 25)
460
+ progress.update("Running logical analysis...", 50)
461
+ progress.update("Detecting issues and behaviours...", 75)
462
+
463
+ result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_service_method(
464
+ service, method_info, progress: progress
465
+ )
466
+
467
+ progress.update("Processing results...", 100)
468
+
469
+ if result&.dig(:error)
470
+ { message: result[:message] || "Audit failed", error: result[:error] }
471
+ else
472
+ { message: "Audit complete!" }
473
+ end
474
+ end
475
+
476
+ return @go_ui.show_auth_error(result[:message] || "Audit failed") if result&.dig(:error)
477
+
478
+ display_audit_results(result)
479
+ end
480
+
481
+ def audit_other_method
482
+ other_files = nil
483
+
484
+ @go_ui.show_spinner("Analyzing other files...") do
485
+ other_files = Tng::Analyzers::Other.files_in_dir.map do |file|
486
+ relative_path = file[:relative_path].gsub(".rb", "")
487
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
488
+
489
+ { name: namespaced_name, path: file[:path], type: file[:type] }
490
+ end
491
+ { success: true, message: "Found #{other_files.length} other files" }
492
+ end
493
+
494
+ if other_files.empty?
495
+ @go_ui.show_no_items("other files")
496
+ return
497
+ end
498
+
499
+ items = other_files.map { |f| { name: f[:name], path: f[:path] } }
500
+ selected_name = @go_ui.show_list_view("Select File", items)
501
+ return if selected_name == "back"
502
+
503
+ other_choice = other_files.find { |f| f[:name] == selected_name }
504
+ return unless other_choice
505
+
506
+ show_audit_other_method_selection(other_choice)
507
+ end
508
+
509
+ def show_audit_other_method_selection(other_file)
510
+ methods = extract_other_methods(other_file)
511
+
512
+ if methods.empty?
513
+ @go_ui.show_no_items("methods in #{other_file[:name]}")
514
+ return
515
+ end
516
+
517
+ items = methods.map { |m| { name: m[:name], path: other_file[:name] } }
518
+ selected_name = @go_ui.show_list_view("Audit Method in #{other_file[:name]}", items)
519
+ return if selected_name == "back"
520
+
521
+ method_choice = methods.find { |m| m[:name] == selected_name }
522
+ return unless method_choice
523
+
524
+ run_audit_for_other_method(other_file, method_choice)
525
+ end
526
+
527
+ def run_audit_for_other_method(other_file, method_info)
528
+ result = nil
529
+
530
+ @go_ui.show_progress("Auditing #{other_file[:name]}##{method_info[:name]}") do |progress|
531
+ progress.update("Parsing method details...", 25)
532
+ progress.update("Analyzing method context...", 25)
533
+ progress.update("Running logical analysis...", 50)
534
+ progress.update("Detecting issues and behaviours...", 75)
535
+
536
+ result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_other_method(
537
+ other_file, method_info, progress: progress
538
+ )
539
+
540
+ progress.update("Processing results...", 100)
541
+
542
+ if result&.dig(:error)
543
+ { message: result[:message] || "Audit failed", error: result[:error] }
544
+ else
545
+ { message: "Audit complete!" }
546
+ end
547
+ end
548
+
549
+ return @go_ui.show_auth_error(result[:message] || "Audit failed") if result&.dig(:error)
550
+
551
+ display_audit_results(result)
552
+ end
553
+
204
554
  def generate_controller_tests
205
555
  controllers = nil
206
556
 
@@ -220,13 +570,16 @@ class CLI
220
570
  end
221
571
 
222
572
  items = controllers.map { |c| { name: c[:name], path: c[:path] } }
223
- selected_name = @go_ui.show_list_view("Select Controller", items)
224
- return if selected_name == "back"
225
573
 
226
- controller_choice = controllers.find { |c| c[:name] == selected_name }
227
- return unless controller_choice
574
+ loop do
575
+ selected_name = @go_ui.show_list_view("Select Controller", items)
576
+ return if selected_name == "back"
577
+
578
+ controller_choice = controllers.find { |c| c[:name] == selected_name }
579
+ next unless controller_choice
228
580
 
229
- show_controller_test_options(controller_choice)
581
+ show_controller_test_options(controller_choice)
582
+ end
230
583
  end
231
584
 
232
585
  def generate_model_tests
@@ -248,13 +601,16 @@ class CLI
248
601
  end
249
602
 
250
603
  items = models.map { |m| { name: m[:name], path: m[:path] } }
251
- selected_name = @go_ui.show_list_view("Select Model", items)
252
- return if selected_name == "back"
253
604
 
254
- model_choice = models.find { |m| m[:name] == selected_name }
255
- return unless model_choice
605
+ loop do
606
+ selected_name = @go_ui.show_list_view("Select Model", items)
607
+ return if selected_name == "back"
608
+
609
+ model_choice = models.find { |m| m[:name] == selected_name }
610
+ next unless model_choice
256
611
 
257
- show_model_test_options(model_choice)
612
+ show_model_test_options(model_choice)
613
+ end
258
614
  end
259
615
 
260
616
  def generate_service_tests
@@ -276,13 +632,16 @@ class CLI
276
632
  end
277
633
 
278
634
  items = services.map { |s| { name: s[:name], path: s[:path] } }
279
- selected_name = @go_ui.show_list_view("Select Service", items)
280
- return if selected_name == "back"
281
635
 
282
- service_choice = services.find { |s| s[:name] == selected_name }
283
- return unless service_choice
636
+ loop do
637
+ selected_name = @go_ui.show_list_view("Select Service", items)
638
+ return if selected_name == "back"
284
639
 
285
- show_service_test_options(service_choice)
640
+ service_choice = services.find { |s| s[:name] == selected_name }
641
+ next unless service_choice
642
+
643
+ show_service_test_options(service_choice)
644
+ end
286
645
  end
287
646
 
288
647
  def generate_other_tests
@@ -623,7 +982,8 @@ class CLI
623
982
  @testng,
624
983
  @http_client,
625
984
  params,
626
- method(:show_post_generation_menu)
985
+ method(:show_post_generation_menu),
986
+ @go_ui
627
987
  )
628
988
 
629
989
  direct_generator.run
@@ -688,6 +1048,18 @@ class CLI
688
1048
  )
689
1049
  @testng = Services::Testng.new(@http_client)
690
1050
  end
1051
+
1052
+ def handle_fix_command
1053
+ file_path = params[:file]
1054
+ unless file_path
1055
+ puts @pastel.red("❌ File parameter is required for fix mode")
1056
+ puts @pastel.yellow("Usage: bundle exec tng --fix --file=your_file.rb")
1057
+ return
1058
+ end
1059
+
1060
+ orchestrator = Tng::Services::FixOrchestrator.new(@pastel, @http_client)
1061
+ orchestrator.run(file_path)
1062
+ end
691
1063
  end
692
1064
 
693
1065
  cli = CLI.new
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/binaries/tng.bundle CHANGED
Binary file
@@ -8,16 +8,29 @@ module Tng
8
8
  class DirectGeneration
9
9
  include ExtractMethods
10
10
 
11
- def initialize(pastel, testng, http_client, params, show_post_generation_menu_proc)
11
+ def initialize(pastel, testng, http_client, params, show_post_generation_menu_proc, go_ui)
12
12
  @pastel = pastel
13
13
  @testng = testng
14
14
  @http_client = http_client
15
15
  @params = params
16
16
  @show_post_generation_menu = show_post_generation_menu_proc
17
+ @go_ui = go_ui
18
+ end
19
+
20
+ # ... (rest of methods)
21
+
22
+ def display_audit_results(result)
23
+ audit_results = result[:audit_results]
24
+ return unless audit_results
25
+
26
+ # Use Go UI to display rich audit results
27
+ # "issues" type handles both issues and behaviours in the unified view
28
+ @go_ui.show_audit_results(audit_results, "issues")
17
29
  end
18
30
 
19
31
  def run
20
- file_path, method_name = @params[:file], @params[:method]
32
+ file_path = @params[:file]
33
+ method_name = @params[:method]
21
34
 
22
35
  unless file_path && method_name
23
36
  puts @pastel.red("❌ Both file and method parameters are required")
@@ -42,7 +55,7 @@ module Tng
42
55
  private
43
56
 
44
57
  def suggest_similar_files(file_path)
45
- base_name = File.basename(file_path, '.rb')
58
+ base_name = File.basename(file_path, ".rb")
46
59
  puts @pastel.yellow("💡 Did you mean one of these?")
47
60
 
48
61
  similar_files = find_similar_files(base_name)
@@ -61,8 +74,8 @@ module Tng
61
74
  %w[app/controllers app/models app/services app/service].each do |dir|
62
75
  next unless Dir.exist?(File.join(rails_root, dir))
63
76
 
64
- Dir.glob(File.join(rails_root, dir, '**', "*#{base_name}*.rb")).each do |file|
65
- similar_files << file.gsub(/^#{Regexp.escape(rails_root)}\//, '')
77
+ Dir.glob(File.join(rails_root, dir, "**", "*#{base_name}*.rb")).each do |file|
78
+ similar_files << file.gsub(%r{^#{Regexp.escape(rails_root)}/}, "")
66
79
  end
67
80
  end
68
81
 
@@ -87,11 +100,17 @@ module Tng
87
100
  return
88
101
  end
89
102
 
90
- puts @pastel.bright_white("🎯 Generating test for #{file_object[:name]}##{method_info[:name]}...")
103
+ puts @pastel.bright_white("🎯 #{@params[:audit] ? "Auditing" : "Generating test for"} #{file_object[:name]}##{method_info[:name]}...")
91
104
 
92
105
  result = generate_test_result(file_object, method_info, type)
93
106
 
94
- if result && result[:file_path]
107
+ if result&.dig(:error)
108
+ puts @pastel.red("❌ #{@params[:audit] ? "Audit" : "Test generation"} failed: #{result[:message]}")
109
+ elsif @params[:audit]
110
+ # Audit mode - display results inline
111
+ display_audit_results(result)
112
+ elsif result && result[:file_path]
113
+ # Test generation mode - show post-generation menu
95
114
  @show_post_generation_menu.call(result)
96
115
  else
97
116
  puts @pastel.red("❌ Failed to generate test")
@@ -110,11 +129,20 @@ module Tng
110
129
  def generate_test_result(file_object, method_info, type)
111
130
  generator = Tng::Services::TestGenerator.new(@http_client)
112
131
 
113
- case type
114
- when "controller" then generator.run_for_controller_method(file_object, method_info)
115
- when "model" then generator.run_for_model_method(file_object, method_info)
116
- when "service" then generator.run_for_service_method(file_object, method_info)
117
- else generator.run_for_other_method(file_object, method_info)
132
+ if @params[:audit]
133
+ # Audit mode - return issues and behaviours
134
+ case type
135
+ when "controller" then generator.run_audit_for_controller_method(file_object, method_info)
136
+ else { error: :unsupported, message: "Audit mode only supports controllers currently" }
137
+ end
138
+ else
139
+ # Test generation mode
140
+ case type
141
+ when "controller" then generator.run_for_controller_method(file_object, method_info)
142
+ when "model" then generator.run_for_model_method(file_object, method_info)
143
+ when "service" then generator.run_for_service_method(file_object, method_info)
144
+ else generator.run_for_other_method(file_object, method_info)
145
+ end
118
146
  end
119
147
  end
120
148
  end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "repair_service"
4
+
5
+ module Tng
6
+ module Services
7
+ class FixOrchestrator
8
+ MAX_ITERATIONS = 3
9
+
10
+ def initialize(pastel, http_client)
11
+ @pastel = pastel
12
+ @repair_service = RepairService.new(http_client)
13
+ end
14
+
15
+ def run(file_path)
16
+ unless File.exist?(file_path)
17
+ puts @pastel.red("❌ File not found: #{file_path}")
18
+ return
19
+ end
20
+
21
+ puts @pastel.bright_white("🔧 Starting Auto-Fix loop for #{file_path}...")
22
+
23
+ all_healthy = false
24
+ MAX_ITERATIONS.times do |i|
25
+ puts @pastel.dim("\n--- Iteration #{i + 1} ---")
26
+
27
+ # 1. Syntax Check
28
+ status = Tng::Utils.validate_ruby_syntax(file_path)
29
+ unless status[:success]
30
+ puts @pastel.yellow("⚠️ Syntax error detected!")
31
+ apply_fix(file_path, :syntax, status[:output])
32
+ next
33
+ end
34
+ puts @pastel.green("✓ Syntax OK")
35
+
36
+ # 2. Rubocop Check
37
+ status = Tng::Utils.validate_rubocop(file_path)
38
+ unless status[:success]
39
+ puts @pastel.yellow("⚠️ Rubocop violations detected!")
40
+ apply_fix(file_path, :lint, status[:output])
41
+ next
42
+ end
43
+ puts @pastel.green("✓ Rubocop OK")
44
+
45
+ # 3. Test Check (Optional/Proactive)
46
+ status = Tng::Utils.run_tests(file_path)
47
+ unless status[:success]
48
+ puts @pastel.yellow("⚠️ Test failures detected!")
49
+ apply_fix(file_path, :runtime, status[:output])
50
+ next
51
+ end
52
+ puts @pastel.green("✓ Tests Passed")
53
+
54
+ all_healthy = true
55
+ break
56
+ end
57
+
58
+ if all_healthy
59
+ puts @pastel.bright_green("\n🎉 All checks passed! File is healthy.")
60
+ else
61
+ puts @pastel.red("\n❌ Max iterations reached. File still has issues.")
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def apply_fix(file_path, error_type, error_message)
68
+ puts @pastel.blue("🧠 Calling LLM to fix #{error_type}...")
69
+
70
+ context = {
71
+ app_config: Tng::Services::UserAppConfig.config_with_source,
72
+ test_gems: Tng::Utils.has_gem?("rspec") ? ["rspec"] : ["minitest"] # Basic for now
73
+ }
74
+
75
+ result = @repair_service.repair_file(file_path, error_type, error_message, context)
76
+
77
+ if result[:error]
78
+ puts @pastel.red("❌ Repair failed: #{result[:message]}")
79
+ return
80
+ end
81
+
82
+ if result["file_content"]
83
+ File.write(file_path, result["file_content"])
84
+ puts @pastel.green("✅ Fix applied: #{result["applied_fixes"]&.join(", ")}")
85
+ puts @pastel.dim("📝 Explanation: #{result["explanation"]}")
86
+ else
87
+ puts @pastel.yellow("⚠️ LLM did not return file content.")
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "json"
5
+
6
+ module Tng
7
+ module Services
8
+ class RepairService
9
+ REPAIR_PATH = "cli/tng_rails/contents/repair"
10
+
11
+ def initialize(http_client)
12
+ @http_client = http_client
13
+ end
14
+
15
+ def repair_file(file_path, error_type, error_message, context_info = {})
16
+ file_content = File.read(file_path)
17
+
18
+ payload = {
19
+ file_path: file_path,
20
+ file_content: file_content,
21
+ error_type: error_type,
22
+ error_message: error_message,
23
+ context_info: context_info
24
+ }
25
+
26
+ # Compress payload
27
+ json_payload = payload.to_json
28
+ compressed_payload = Zlib::Deflate.deflate(json_payload)
29
+
30
+ response = @http_client.post_binary(REPAIR_PATH, compressed_payload)
31
+
32
+ return { error: :network_error, message: "Network error" } if response.is_a?(HTTPX::ErrorResponse)
33
+ return { error: :auth_failed, message: "Auth failed" } if [401, 403].include?(response.status)
34
+
35
+ begin
36
+ data = JSON.parse(response.body)
37
+ if data["error"]
38
+ { error: :server_error, message: data["error"] }
39
+ else
40
+ data["result"] # This contains { file_content, explanation, applied_fixes }
41
+ end
42
+ rescue JSON::ParserError => e
43
+ { error: :parse_error, message: "Failed to parse repair response: #{e.message}" }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -36,6 +36,70 @@ module Tng
36
36
  generate_test_for_type(other_file, method_info, :other, progress: progress)
37
37
  end
38
38
 
39
+ # Audit mode - async, polls for completion
40
+ def run_audit_for_controller_method(controller, method_info, progress: nil)
41
+ run_audit_for_type(controller, method_info, :controller, progress: progress)
42
+ end
43
+
44
+ def run_audit_for_model_method(model, method_info, progress: nil)
45
+ run_audit_for_type(model, method_info, :model, progress: progress)
46
+ end
47
+
48
+ def run_audit_for_service_method(service, method_info, progress: nil)
49
+ run_audit_for_type(service, method_info, :service, progress: progress)
50
+ end
51
+
52
+ def run_audit_for_other_method(other_file, method_info, progress: nil)
53
+ run_audit_for_type(other_file, method_info, :other, progress: progress)
54
+ end
55
+
56
+ def run_audit_for_type(file_object, method_info, type, progress: nil)
57
+ response = send_audit_request_for_type(file_object, method_info, type)
58
+ return { error: :network_error, message: "Network error" } unless response
59
+
60
+ if response.is_a?(HTTPX::ErrorResponse)
61
+ return { error: :network_error, message: response.error&.message || "Network error" }
62
+ end
63
+
64
+ return { error: :auth_failed, message: "Invalid or missing API key" } if response.status == 401
65
+ return { error: :auth_failed, message: "API key expired" } if response.status == 403
66
+
67
+ begin
68
+ job_data = JSON.parse(response.body)
69
+ return { error: :server_error, message: job_data["error"] } if job_data["error"]
70
+
71
+ job_id = job_data["job_id"]
72
+ return { error: :server_error, message: "No job_id returned" } unless job_id
73
+
74
+ # Poll for completion (similar to test generation)
75
+ result = poll_for_completion(job_id, progress: progress)
76
+ return { error: :timeout, message: "Audit timed out" } unless result
77
+
78
+ # Audit results come back as the result directly (not wrapped)
79
+ # Wrap in audit_results key for save_audit_file compatibility
80
+ { audit_results: result }
81
+ rescue JSON::ParserError => e
82
+ { error: :parse_error, message: "Failed to parse audit response: #{e.message}" }
83
+ end
84
+ end
85
+
86
+ def send_audit_request_for_type(file_object, method_info, type)
87
+ config = request_config
88
+ name = file_object[:name] || File.basename(file_object[:path], ".rb")
89
+
90
+ # Pass audit_mode: true as 8th parameter
91
+ case type
92
+ when :controller
93
+ Tng.send_request_for_controller(name, file_object[:path], method_info, *config, true)
94
+ when :model
95
+ Tng.send_request_for_model(name, file_object[:path], method_info, *config, true)
96
+ when :service
97
+ Tng.send_request_for_service(name, file_object[:path], method_info, *config, true)
98
+ when :other
99
+ Tng.send_request_for_other(name, file_object[:path], method_info, *config, true)
100
+ end
101
+ end
102
+
39
103
  def generate_test_for_type(file_object, method_info, type, progress: nil)
40
104
  start_time = Time.now
41
105
 
@@ -77,15 +141,16 @@ module Tng
77
141
  config = request_config
78
142
  name = file_object[:name] || File.basename(file_object[:path], ".rb")
79
143
 
144
+ # Pass audit_mode: false as 8th parameter for normal test generation
80
145
  case type
81
146
  when :controller
82
- Tng.send_request_for_controller(name, file_object[:path], method_info, *config)
147
+ Tng.send_request_for_controller(name, file_object[:path], method_info, *config, false)
83
148
  when :model
84
- Tng.send_request_for_model(name, file_object[:path], method_info, *config)
149
+ Tng.send_request_for_model(name, file_object[:path], method_info, *config, false)
85
150
  when :service
86
- Tng.send_request_for_service(name, file_object[:path], method_info, *config)
151
+ Tng.send_request_for_service(name, file_object[:path], method_info, *config, false)
87
152
  when :other
88
- Tng.send_request_for_other(name, file_object[:path], method_info, *config)
153
+ Tng.send_request_for_other(name, file_object[:path], method_info, *config, false)
89
154
  end
90
155
  end
91
156
 
@@ -98,6 +163,7 @@ module Tng
98
163
  ]
99
164
  end
100
165
 
166
+ # Modified method signature and added interrupt handler
101
167
  def poll_for_completion(job_id, progress: nil)
102
168
  start_time = Time.current
103
169
  attempts = 0
@@ -229,10 +295,26 @@ module Tng
229
295
  next
230
296
  end
231
297
  rescue JSON::ParserError => e
232
- debug_log("Failed to parse response status: #{e.message}") if debug_enabled?
298
+ debug_log("JSON parse error: #{e.message}") if debug_enabled?
233
299
  next
234
300
  end
235
301
  end
302
+ rescue Interrupt
303
+ # User pressed Ctrl+C - exit gracefully
304
+ system("clear") || system("cls")
305
+ puts "\n\n#{TTY::Box.frame(
306
+ "Generation cancelled by user",
307
+ padding: 1,
308
+ align: :center,
309
+ border: :thick,
310
+ style: {
311
+ fg: :yellow,
312
+ border: {
313
+ fg: :yellow
314
+ }
315
+ }
316
+ )}"
317
+ exit(0)
236
318
  end
237
319
 
238
320
  def trigger_cleanup(job_id)
@@ -47,14 +47,15 @@ module Tng
47
47
  end
48
48
 
49
49
  # Show test type selection menu
50
+ # @param mode [String] "test" or "audit" (default: "test")
50
51
  # Returns: "controller", "model", "service", "other", "back"
51
- def show_test_type_menu
52
+ def show_test_type_menu(mode = "test")
52
53
  output_file = Tempfile.new(["go_ui_test_type", ".json"])
53
54
  output_path = output_file.path
54
55
  output_file.close
55
56
 
56
57
  begin
57
- system(@binary_path, "rails-test-menu", "--output", output_path)
58
+ system(@binary_path, "rails-test-menu", "--output", output_path, "--mode", mode)
58
59
 
59
60
  return "back" unless File.exist?(output_path) && File.size(output_path) > 0
60
61
 
@@ -92,7 +93,7 @@ module Tng
92
93
  # @param message [String] Spinner message
93
94
  # @yield Block that returns Hash with :success and :message keys
94
95
  # @return [Hash] Result from block
95
- def show_spinner(message, &block)
96
+ def show_spinner(message)
96
97
  control_file = Tempfile.new(["go_ui_spinner", ".json"])
97
98
  control_path = control_file.path
98
99
  control_file.close
@@ -126,7 +127,7 @@ module Tng
126
127
  # @param title [String] Progress title
127
128
  # @yield [ProgressUpdater] Block receives updater and returns Hash with :message and :result
128
129
  # @return [Hash] Result from block
129
- def show_progress(title, &block)
130
+ def show_progress(title)
130
131
  control_file = Tempfile.new(["go_ui_progress", ".json"])
131
132
  control_path = control_file.path
132
133
  control_file.close
@@ -239,6 +240,28 @@ module Tng
239
240
  puts "Test results error: #{e.message}"
240
241
  end
241
242
 
243
+ # Show audit results
244
+ # @param audit_result [Hash] Full audit result with items, method_name, class_name, method_source_with_lines
245
+ # @param audit_type [String] "issues" or "behaviours"
246
+ def show_audit_results(audit_result, _audit_type)
247
+ # Always use the unified audit-results command which handles both issues and behaviours
248
+ command = "audit-results"
249
+
250
+ # Send full result object via temp file to avoid CLI argument limits
251
+ data_json = JSON.generate(audit_result)
252
+
253
+ input_file = Tempfile.new(["go_ui_audit", ".json"])
254
+ input_path = input_file.path
255
+ File.write(input_path, data_json)
256
+ input_file.close
257
+
258
+ system(@binary_path, command, "--file", input_path)
259
+ rescue StandardError => e
260
+ puts "Audit results error: #{e.message}"
261
+ ensure
262
+ File.unlink(input_path) if input_path && File.exist?(input_path)
263
+ end
264
+
242
265
  # Show authentication error
243
266
  # @param message [String] Error message to display
244
267
  def show_auth_error(message = "Authentication failed")
data/lib/tng/utils.rb CHANGED
@@ -287,7 +287,7 @@ module Tng
287
287
  end
288
288
  end
289
289
 
290
- private_class_method def self.count_test_nodes(node)
290
+ def self.count_test_nodes(node)
291
291
  return 0 unless node.respond_to?(:child_nodes)
292
292
 
293
293
  count = 0
@@ -303,7 +303,7 @@ module Tng
303
303
  count
304
304
  end
305
305
 
306
- private_class_method def self.test_node?(node)
306
+ def self.test_node?(node)
307
307
  case node
308
308
  when Prism::DefNode
309
309
  # Minitest: def test_something
@@ -456,5 +456,28 @@ module Tng
456
456
  "#{minutes}m #{remaining_seconds}s"
457
457
  end
458
458
  end
459
+
460
+ def self.validate_ruby_syntax(file_path)
461
+ output = `ruby -c #{file_path} 2>&1`
462
+ { success: $?.success?, output: output }
463
+ end
464
+
465
+ def self.validate_rubocop(file_path)
466
+ # Run rubocop with JSON output
467
+ output = `bundle exec rubocop #{file_path} --format json 2>&1`
468
+ { success: $?.success?, output: output }
469
+ rescue StandardError => e
470
+ { success: false, output: "Rubocop error: #{e.message}" }
471
+ end
472
+
473
+ def self.run_tests(file_path)
474
+ command = if file_path.include?("/spec/")
475
+ "bundle exec rspec #{file_path}"
476
+ else
477
+ "bundle exec rails test #{file_path}"
478
+ end
479
+ output = `#{command} 2>&1`
480
+ { success: $?.success?, output: output, command: command }
481
+ end
459
482
  end
460
483
  end
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.8"
4
+ VERSION = "0.3.9"
5
5
  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.8
4
+ version: 0.3.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - ralucab
@@ -176,6 +176,8 @@ files:
176
176
  - lib/tng/services/direct_generation.rb
177
177
  - lib/tng/services/extract_methods.rb
178
178
  - lib/tng/services/file_type_detector.rb
179
+ - lib/tng/services/fix_orchestrator.rb
180
+ - lib/tng/services/repair_service.rb
179
181
  - lib/tng/services/test_generator.rb
180
182
  - lib/tng/services/testng.rb
181
183
  - lib/tng/services/user_app_config.rb
@@ -217,7 +219,7 @@ post_install_message: "┌ TNG ────────────────
217
219
  \ │\n│ • bundle exec
218
220
  tng --help - Show help information │\n│ │\n│
219
221
  \ \U0001F4A1 Generate tests for individual methods with precision │\n└────────────────────────────────────────────────────────────
220
- v0.3.8 ┘\n"
222
+ v0.3.9 ┘\n"
221
223
  rdoc_options: []
222
224
  require_paths:
223
225
  - lib