tng 0.3.8 → 0.4.0

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: 4090a9996518027b0e5d49534360ce2269a735c407dcdb5544e4842a1431b590
4
+ data.tar.gz: 1171ff1e89fe751cb2a4de7a8cb688db9c65b22babdb7c1164f5d24032cc1092
5
5
  SHA512:
6
- metadata.gz: 868acc7d7fa9b539fac0b9906662cdf906a115da97f86643f8d2c88f9015b536e3ec75bbb50537df0a2283232207d7531a12b81eb8af4bdc8a1aa4abb4a83122
7
- data.tar.gz: 1410ad3af1e8b0bfbfa7d9849ba293cddca599e4dfda976f6433651814343592e39ef40aec5ca7c884e4ad28f004ab86bd36a384e1c334ab008629bec9ea3bd5
6
+ metadata.gz: af6285fd48d5c93a1fad7698b1636aa03fef7f8f6f2b4705ed68390d1b0ee78a2aa118836952c7b2aac63dfaec417f23d9eaf99134e604ce43bed85576b3ddb6
7
+ data.tar.gz: 24f76ab0623a752f753b26c84b22ec8b971970de6e8493ea0f52c024eb94ee5fcd3f8459d4845782d01dcc23e92f8ac551136fec3a533be7b0ebac09812c973f
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,23 @@ 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 :json do
68
+ long "--json"
69
+ desc "Output results in JSON format"
70
+ end
71
+
72
+ flag :fix do
73
+ short "-x"
74
+ long "--fix"
75
+ desc "Run in fix mode (automatically repair syntax, lint, and test errors)"
76
+ end
77
+
60
78
  flag :help do
61
79
  short "-h"
62
80
  long "--help"
@@ -65,7 +83,13 @@ class CLI
65
83
 
66
84
  def initialize
67
85
  @pastel = Pastel.new
68
- @go_ui = Tng::UI::GoUISession.new
86
+ # Check for --json flag raw before parsing
87
+ if ARGV.any? { |arg| arg == "--json" }
88
+ require "tng/ui/json_session"
89
+ @go_ui = Tng::UI::JsonSession.new
90
+ else
91
+ @go_ui = Tng::UI::GoUISession.new
92
+ end
69
93
  @terminal_width = begin
70
94
  TTY::Screen.width
71
95
  rescue StandardError
@@ -90,7 +114,9 @@ class CLI
90
114
 
91
115
  initialize_config_and_clients if rails_loaded
92
116
 
93
- if params[:file]
117
+ if params[:fix]
118
+ handle_fix_command
119
+ elsif params[:file]
94
120
  run_direct_generation
95
121
  else
96
122
  if find_rails_root && !rails_loaded && !defined?(Rails)
@@ -121,6 +147,12 @@ class CLI
121
147
  normalized << "--file=#{::Regexp.last_match(2)}"
122
148
  when /^(?:--)?(method|m)=(.+)$/
123
149
  normalized << "--method=#{::Regexp.last_match(2)}"
150
+ when /^(?:--)?(audit|a)$/
151
+ normalized << "--audit"
152
+ when /^(?:--)?(json)$/
153
+ normalized << "--json"
154
+ when /^(?:--)?(fix|x)$/
155
+ normalized << "--fix"
124
156
  when /^(help|h)=(.+)$/
125
157
  normalized << "--help=#{::Regexp.last_match(2)}"
126
158
  when /^--file$/, /^-f$/
@@ -151,8 +183,11 @@ class CLI
151
183
  arg = positional_args[0]
152
184
  has_file = normalized.any? { |a| a.match?(/^--file/) }
153
185
  has_method = normalized.any? { |a| a.match?(/^--method/) }
186
+ has_fix = normalized.any? { |a| a.match?(/^--fix/) }
154
187
 
155
- if !has_file && (arg.end_with?(".rb") || arg.include?("/"))
188
+ if has_fix && !has_file
189
+ normalized << "--file=#{arg}"
190
+ elsif !has_file && (arg.end_with?(".rb") || arg.include?("/"))
156
191
  normalized << "--file=#{arg}"
157
192
  elsif !has_method && !arg.include?("/")
158
193
  normalized << "--method=#{arg}"
@@ -171,6 +206,8 @@ class CLI
171
206
  case choice
172
207
  when "tests"
173
208
  handle_test_generation
209
+ when "audit"
210
+ handle_audit_method
174
211
  when "stats"
175
212
  user_stats
176
213
  when "about"
@@ -201,6 +238,332 @@ class CLI
201
238
  end
202
239
  end
203
240
 
241
+ def handle_audit_method
242
+ choice = @go_ui.show_test_type_menu("audit")
243
+
244
+ case choice
245
+ when "controller"
246
+ audit_controller_method
247
+ when "model"
248
+ audit_model_method
249
+ when "service"
250
+ audit_service_method
251
+ when "other"
252
+ audit_other_method
253
+ end
254
+ end
255
+
256
+ def audit_controller_method
257
+ controllers = nil
258
+
259
+ @go_ui.show_spinner("Analyzing controllers...") do
260
+ controllers = Tng::Analyzers::Controller.files_in_dir("app/controllers").map do |file|
261
+ relative_path = file[:path].gsub(%r{^.*app/controllers/}, "").gsub(".rb", "")
262
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
263
+ { name: namespaced_name, path: file[:path] }
264
+ end
265
+ { success: true, message: "Found #{controllers.length} controllers" }
266
+ end
267
+
268
+ if controllers.empty?
269
+ @go_ui.show_no_items("controllers")
270
+ return
271
+ end
272
+
273
+ items = controllers.map { |c| { name: c[:name], path: c[:path] } }
274
+
275
+ loop do
276
+ selected_name = @go_ui.show_list_view("Select Controller to Audit", items)
277
+ return if selected_name == "back"
278
+
279
+ controller_choice = controllers.find { |c| c[:name] == selected_name }
280
+ next unless controller_choice
281
+
282
+ show_controller_audit_method_selection(controller_choice)
283
+ # If we return here, it means user pressed back from method selection
284
+ # Loop will show controller list again
285
+ end
286
+ end
287
+
288
+ def show_controller_audit_method_selection(controller)
289
+ methods = extract_controller_methods(controller)
290
+
291
+ if methods.empty?
292
+ @go_ui.show_no_items("methods in #{controller[:name]}")
293
+ return
294
+ end
295
+
296
+ items = methods.map { |m| { name: m[:name], path: controller[:name] } }
297
+ selected_name = @go_ui.show_list_view("Select Method to Audit", items)
298
+ return if selected_name == "back"
299
+
300
+ method_choice = methods.find { |m| m[:name] == selected_name }
301
+ return unless method_choice
302
+
303
+ run_audit_for_controller_method(controller, method_choice)
304
+ end
305
+
306
+ def run_audit_for_controller_method(controller, method_info)
307
+ result = nil
308
+
309
+ @go_ui.show_progress("Auditing #{controller[:name]}##{method_info[:name]}") do |progress|
310
+ progress.update("Analyzing method context...", 25)
311
+ progress.update("Running logical analysis...", 50)
312
+ progress.update("Detecting issues and behaviours...", 75)
313
+
314
+ result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_controller_method(
315
+ controller, method_info, progress: progress
316
+ )
317
+
318
+ progress.update("Processing results...", 100)
319
+
320
+ if result&.dig(:error)
321
+ { message: result[:message] || "Audit failed", error: result[:error] }
322
+ else
323
+ { message: "Audit complete!" }
324
+ end
325
+ end
326
+
327
+ return @go_ui.show_auth_error(result[:message] || "Audit failed") if result&.dig(:error)
328
+
329
+ display_audit_results(result)
330
+ end
331
+
332
+ def display_audit_results(result)
333
+ audit_data = result[:audit_results]
334
+
335
+ # Save to JSON for persistence
336
+ File.write("audit.json", JSON.pretty_generate(audit_data))
337
+ puts @pastel.green("💾 Audit results saved to audit.json")
338
+
339
+ # Send entire audit_data to go-ui (includes issues, behaviours, method_name, class_name, method_source_with_lines)
340
+ @go_ui.show_audit_results(audit_data, "issues")
341
+ end
342
+
343
+ # Placeholder methods for other component types
344
+ def audit_model_method
345
+ models = nil
346
+
347
+ @go_ui.show_spinner("Analyzing models...") do
348
+ models = Tng::Analyzers::Model.files_in_dir("app/models").map do |file|
349
+ relative_path = file[:path].gsub(%r{^.*app/models/}, "").gsub(".rb", "")
350
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
351
+ { name: namespaced_name, path: file[:path] }
352
+ end
353
+ { success: true, message: "Found #{models.length} models" }
354
+ end
355
+
356
+ if models.empty?
357
+ @go_ui.show_no_items("models")
358
+ return
359
+ end
360
+
361
+ items = models.map { |m| { name: m[:name], path: m[:path] } }
362
+
363
+ loop do
364
+ selected_name = @go_ui.show_list_view("Select Model to Audit", items)
365
+ return if selected_name == "back"
366
+
367
+ model_choice = models.find { |m| m[:name] == selected_name }
368
+ next unless model_choice
369
+
370
+ show_model_audit_method_selection(model_choice)
371
+ end
372
+ end
373
+
374
+ def show_model_audit_method_selection(model)
375
+ methods = extract_model_methods(model)
376
+
377
+ if methods.empty?
378
+ @go_ui.show_no_items("methods in #{model[:name]}")
379
+ return
380
+ end
381
+
382
+ items = methods.map { |m| { name: m[:name], path: model[:name] } }
383
+ selected_name = @go_ui.show_list_view("Select Method to Audit", items)
384
+ return if selected_name == "back"
385
+
386
+ method_choice = methods.find { |m| m[:name] == selected_name }
387
+ return unless method_choice
388
+
389
+ run_audit_for_model_method(model, method_choice)
390
+ end
391
+
392
+ def run_audit_for_model_method(model, method_info)
393
+ result = nil
394
+
395
+ @go_ui.show_progress("Auditing #{model[:name]}##{method_info[:name]}") do |progress|
396
+ progress.update("Parsing method details...", 25)
397
+ progress.update("Analyzing method context...", 25)
398
+ progress.update("Running logical analysis...", 50)
399
+ progress.update("Detecting issues and behaviours...", 75)
400
+
401
+ result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_model_method(
402
+ model, method_info, progress: progress
403
+ )
404
+
405
+ progress.update("Processing results...", 100)
406
+
407
+ if result&.dig(:error)
408
+ { message: result[:message] || "Audit failed", error: result[:error] }
409
+ else
410
+ { message: "Audit complete!" }
411
+ end
412
+ end
413
+
414
+ return @go_ui.show_auth_error(result[:message] || "Audit failed") if result&.dig(:error)
415
+
416
+ display_audit_results(result)
417
+ end
418
+
419
+ def audit_service_method
420
+ services = nil
421
+
422
+ @go_ui.show_spinner("Analyzing services...") do
423
+ services = Tng::Analyzers::Service.files_in_dir.map do |file|
424
+ relative_path = file[:path].gsub(%r{^.*app/services?/}, "").gsub(".rb", "")
425
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
426
+ { name: namespaced_name, path: file[:path] }
427
+ end
428
+ { success: true, message: "Found #{services.length} services" }
429
+ end
430
+
431
+ if services.empty?
432
+ @go_ui.show_no_items("services")
433
+ return
434
+ end
435
+
436
+ items = services.map { |s| { name: s[:name], path: s[:path] } }
437
+
438
+ loop do
439
+ selected_name = @go_ui.show_list_view("Select Service to Audit", items)
440
+ return if selected_name == "back"
441
+
442
+ service_choice = services.find { |s| s[:name] == selected_name }
443
+ next unless service_choice
444
+
445
+ show_service_audit_method_selection(service_choice)
446
+ end
447
+ end
448
+
449
+ def show_service_audit_method_selection(service)
450
+ methods = extract_service_methods(service)
451
+
452
+ if methods.empty?
453
+ @go_ui.show_no_items("methods in #{service[:name]}")
454
+ return
455
+ end
456
+
457
+ items = methods.map { |m| { name: m[:name], path: service[:name] } }
458
+ selected_name = @go_ui.show_list_view("Select Method to Audit", items)
459
+ return if selected_name == "back"
460
+
461
+ method_choice = methods.find { |m| m[:name] == selected_name }
462
+ return unless method_choice
463
+
464
+ run_audit_for_service_method(service, method_choice)
465
+ end
466
+
467
+ def run_audit_for_service_method(service, method_info)
468
+ result = nil
469
+
470
+ @go_ui.show_progress("Auditing #{service[:name]}##{method_info[:name]}") do |progress|
471
+ progress.update("Parsing method details...", 25)
472
+ progress.update("Analyzing method context...", 25)
473
+ progress.update("Running logical analysis...", 50)
474
+ progress.update("Detecting issues and behaviours...", 75)
475
+
476
+ result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_service_method(
477
+ service, method_info, progress: progress
478
+ )
479
+
480
+ progress.update("Processing results...", 100)
481
+
482
+ if result&.dig(:error)
483
+ { message: result[:message] || "Audit failed", error: result[:error] }
484
+ else
485
+ { message: "Audit complete!" }
486
+ end
487
+ end
488
+
489
+ return @go_ui.show_auth_error(result[:message] || "Audit failed") if result&.dig(:error)
490
+
491
+ display_audit_results(result)
492
+ end
493
+
494
+ def audit_other_method
495
+ other_files = nil
496
+
497
+ @go_ui.show_spinner("Analyzing other files...") do
498
+ other_files = Tng::Analyzers::Other.files_in_dir.map do |file|
499
+ relative_path = file[:relative_path].gsub(".rb", "")
500
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
501
+
502
+ { name: namespaced_name, path: file[:path], type: file[:type] }
503
+ end
504
+ { success: true, message: "Found #{other_files.length} other files" }
505
+ end
506
+
507
+ if other_files.empty?
508
+ @go_ui.show_no_items("other files")
509
+ return
510
+ end
511
+
512
+ items = other_files.map { |f| { name: f[:name], path: f[:path] } }
513
+ selected_name = @go_ui.show_list_view("Select File", items)
514
+ return if selected_name == "back"
515
+
516
+ other_choice = other_files.find { |f| f[:name] == selected_name }
517
+ return unless other_choice
518
+
519
+ show_audit_other_method_selection(other_choice)
520
+ end
521
+
522
+ def show_audit_other_method_selection(other_file)
523
+ methods = extract_other_methods(other_file)
524
+
525
+ if methods.empty?
526
+ @go_ui.show_no_items("methods in #{other_file[:name]}")
527
+ return
528
+ end
529
+
530
+ items = methods.map { |m| { name: m[:name], path: other_file[:name] } }
531
+ selected_name = @go_ui.show_list_view("Audit Method in #{other_file[:name]}", items)
532
+ return if selected_name == "back"
533
+
534
+ method_choice = methods.find { |m| m[:name] == selected_name }
535
+ return unless method_choice
536
+
537
+ run_audit_for_other_method(other_file, method_choice)
538
+ end
539
+
540
+ def run_audit_for_other_method(other_file, method_info)
541
+ result = nil
542
+
543
+ @go_ui.show_progress("Auditing #{other_file[:name]}##{method_info[:name]}") do |progress|
544
+ progress.update("Parsing method details...", 25)
545
+ progress.update("Analyzing method context...", 25)
546
+ progress.update("Running logical analysis...", 50)
547
+ progress.update("Detecting issues and behaviours...", 75)
548
+
549
+ result = Tng::Services::TestGenerator.new(@http_client).run_audit_for_other_method(
550
+ other_file, method_info, progress: progress
551
+ )
552
+
553
+ progress.update("Processing results...", 100)
554
+
555
+ if result&.dig(:error)
556
+ { message: result[:message] || "Audit failed", error: result[:error] }
557
+ else
558
+ { message: "Audit complete!" }
559
+ end
560
+ end
561
+
562
+ return @go_ui.show_auth_error(result[:message] || "Audit failed") if result&.dig(:error)
563
+
564
+ display_audit_results(result)
565
+ end
566
+
204
567
  def generate_controller_tests
205
568
  controllers = nil
206
569
 
@@ -220,13 +583,16 @@ class CLI
220
583
  end
221
584
 
222
585
  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
586
 
226
- controller_choice = controllers.find { |c| c[:name] == selected_name }
227
- return unless controller_choice
587
+ loop do
588
+ selected_name = @go_ui.show_list_view("Select Controller", items)
589
+ return if selected_name == "back"
228
590
 
229
- show_controller_test_options(controller_choice)
591
+ controller_choice = controllers.find { |c| c[:name] == selected_name }
592
+ next unless controller_choice
593
+
594
+ show_controller_test_options(controller_choice)
595
+ end
230
596
  end
231
597
 
232
598
  def generate_model_tests
@@ -248,13 +614,16 @@ class CLI
248
614
  end
249
615
 
250
616
  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
617
 
254
- model_choice = models.find { |m| m[:name] == selected_name }
255
- return unless model_choice
618
+ loop do
619
+ selected_name = @go_ui.show_list_view("Select Model", items)
620
+ return if selected_name == "back"
621
+
622
+ model_choice = models.find { |m| m[:name] == selected_name }
623
+ next unless model_choice
256
624
 
257
- show_model_test_options(model_choice)
625
+ show_model_test_options(model_choice)
626
+ end
258
627
  end
259
628
 
260
629
  def generate_service_tests
@@ -276,13 +645,16 @@ class CLI
276
645
  end
277
646
 
278
647
  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
648
 
282
- service_choice = services.find { |s| s[:name] == selected_name }
283
- return unless service_choice
649
+ loop do
650
+ selected_name = @go_ui.show_list_view("Select Service", items)
651
+ return if selected_name == "back"
284
652
 
285
- show_service_test_options(service_choice)
653
+ service_choice = services.find { |s| s[:name] == selected_name }
654
+ next unless service_choice
655
+
656
+ show_service_test_options(service_choice)
657
+ end
286
658
  end
287
659
 
288
660
  def generate_other_tests
@@ -623,7 +995,8 @@ class CLI
623
995
  @testng,
624
996
  @http_client,
625
997
  params,
626
- method(:show_post_generation_menu)
998
+ method(:show_post_generation_menu),
999
+ @go_ui
627
1000
  )
628
1001
 
629
1002
  direct_generator.run
@@ -688,6 +1061,18 @@ class CLI
688
1061
  )
689
1062
  @testng = Services::Testng.new(@http_client)
690
1063
  end
1064
+
1065
+ def handle_fix_command
1066
+ file_path = params[:file]
1067
+ unless file_path
1068
+ puts @pastel.red("❌ File parameter is required for fix mode")
1069
+ puts @pastel.yellow("Usage: bundle exec tng --fix --file=your_file.rb")
1070
+ return
1071
+ end
1072
+
1073
+ orchestrator = Tng::Services::FixOrchestrator.new(@pastel, @http_client)
1074
+ orchestrator.run(file_path)
1075
+ end
691
1076
  end
692
1077
 
693
1078
  cli = CLI.new
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
data/binaries/tng.bundle CHANGED
Binary file
@@ -64,12 +64,6 @@ module Tng
64
64
  name: method_name.to_s
65
65
  }
66
66
  end
67
- rescue NameError => e
68
- puts "❌ Could not load controller class #{controller_name}: #{e.message}"
69
- []
70
- rescue StandardError => e
71
- puts "❌ Error analyzing controller #{controller_name}: #{e.message}"
72
- []
73
67
  end
74
68
  end
75
69
  end
@@ -31,7 +31,8 @@ module Tng
31
31
  model_class = model_name.constantize
32
32
 
33
33
  instance_methods = model_class.public_instance_methods(false) +
34
- model_class.private_instance_methods(false)
34
+ model_class.protected_instance_methods(false) +
35
+ model_class.private_instance_methods(false)
35
36
  class_methods = model_class.public_methods(false) - Class.public_methods
36
37
 
37
38
  model_file = Object.const_source_location(model_class.name)&.first
@@ -78,12 +79,6 @@ module Tng
78
79
  else
79
80
  []
80
81
  end
81
- rescue NameError => e
82
- puts "❌ Could not load model class #{model_name}: #{e.message}"
83
- []
84
- rescue StandardError => e
85
- puts "❌ Error analyzing model #{model_name}: #{e.message}"
86
- []
87
82
  end
88
83
  end
89
84
 
@@ -109,7 +104,7 @@ module Tng
109
104
  node.arguments.arguments.each do |arg|
110
105
  if arg.is_a?(Prism::SymbolNode)
111
106
  validation_method = arg.value
112
- validations << validation_method if validation_method
107
+ validations << "validate :#{validation_method}" if validation_method
113
108
  end
114
109
  end
115
110
  else
@@ -117,7 +112,7 @@ module Tng
117
112
  node.arguments.arguments.each do |arg|
118
113
  if arg.is_a?(Prism::SymbolNode)
119
114
  attr_name = arg.value
120
- validations << attr_name if attr_name
115
+ validations << "validates :#{attr_name}" if attr_name
121
116
  end
122
117
  end
123
118
  end
@@ -32,7 +32,7 @@ module Tng
32
32
  service_class = service_name.constantize
33
33
 
34
34
  instance_methods = service_class.public_instance_methods(false) +
35
- service_class.private_instance_methods(false)
35
+ service_class.private_instance_methods(false)
36
36
  class_methods = service_class.public_methods(false) - Class.public_methods
37
37
 
38
38
  # Try to get source file from any method, fallback to const_source_location
@@ -74,12 +74,6 @@ module Tng
74
74
  end
75
75
 
76
76
  service_methods.map { |method_name| { name: method_name.to_s } }
77
- rescue NameError => e
78
- puts "❌ Could not load service class #{service_name}: #{e.message}"
79
- []
80
- rescue StandardError => e
81
- puts "❌ Error analyzing service #{service_name}: #{e.message}"
82
- []
83
77
  end
84
78
  end
85
79