tng 0.2.1

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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +15 -0
  3. data/LICENSE.md +32 -0
  4. data/README.md +413 -0
  5. data/Rakefile +124 -0
  6. data/bin/load_dev +22 -0
  7. data/bin/tng +888 -0
  8. data/binaries/tng.bundle +0 -0
  9. data/binaries/tng.so +0 -0
  10. data/lib/generators/tng/install_generator.rb +236 -0
  11. data/lib/tng/analyzers/controller.rb +114 -0
  12. data/lib/tng/analyzers/model.rb +131 -0
  13. data/lib/tng/analyzers/other.rb +277 -0
  14. data/lib/tng/analyzers/service.rb +150 -0
  15. data/lib/tng/api/http_client.rb +100 -0
  16. data/lib/tng/railtie.rb +11 -0
  17. data/lib/tng/services/direct_generation.rb +320 -0
  18. data/lib/tng/services/extract_methods.rb +39 -0
  19. data/lib/tng/services/test_generator.rb +287 -0
  20. data/lib/tng/services/testng.rb +100 -0
  21. data/lib/tng/services/user_app_config.rb +76 -0
  22. data/lib/tng/ui/about_display.rb +66 -0
  23. data/lib/tng/ui/authentication_warning_display.rb +172 -0
  24. data/lib/tng/ui/configuration_display.rb +52 -0
  25. data/lib/tng/ui/controller_test_flow_display.rb +79 -0
  26. data/lib/tng/ui/display_banner.rb +44 -0
  27. data/lib/tng/ui/goodbye_display.rb +41 -0
  28. data/lib/tng/ui/model_test_flow_display.rb +80 -0
  29. data/lib/tng/ui/other_test_flow_display.rb +78 -0
  30. data/lib/tng/ui/post_install_box.rb +80 -0
  31. data/lib/tng/ui/service_test_flow_display.rb +78 -0
  32. data/lib/tng/ui/show_help.rb +78 -0
  33. data/lib/tng/ui/system_status_display.rb +128 -0
  34. data/lib/tng/ui/theme.rb +258 -0
  35. data/lib/tng/ui/user_stats_display.rb +160 -0
  36. data/lib/tng/utils.rb +325 -0
  37. data/lib/tng/version.rb +5 -0
  38. data/lib/tng.rb +308 -0
  39. data/tng.gemspec +56 -0
  40. metadata +293 -0
data/bin/tng ADDED
@@ -0,0 +1,888 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $0 = "tng"
5
+
6
+ lib_path = File.expand_path("../lib", __dir__)
7
+ $LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
8
+
9
+ require "tng"
10
+ require "tng/services/direct_generation"
11
+ require "tng/services/extract_methods"
12
+
13
+ require "tty-prompt"
14
+ require "tty-spinner"
15
+ require "tty-box"
16
+ require "tty-table"
17
+ require "tty-progressbar"
18
+ require "tty-screen"
19
+ require "tty-option"
20
+ require "pastel"
21
+ require "tng/ui/show_help"
22
+ require "tng/ui/post_install_box"
23
+ require "tng/ui/theme"
24
+ require "tng/ui/system_status_display"
25
+ require "tng/ui/configuration_display"
26
+ require "tng/ui/authentication_warning_display"
27
+ require "tng/ui/display_banner"
28
+ require "tng/ui/user_stats_display"
29
+ require "tng/ui/about_display"
30
+ require "tng/services/testng"
31
+ require "tng/ui/goodbye_display"
32
+ require "tng/api/http_client"
33
+ require "tng/ui/controller_test_flow_display"
34
+ require "tng/ui/model_test_flow_display"
35
+ require "tng/ui/service_test_flow_display"
36
+ require "tng/ui/other_test_flow_display"
37
+ require "tng/analyzers/controller"
38
+ require "tng/analyzers/model"
39
+ require "tng/analyzers/service"
40
+ require "tng/analyzers/other"
41
+ require "tng/services/test_generator"
42
+ require "tng/services/user_app_config"
43
+ require "tng/utils"
44
+
45
+ class CLI
46
+ include Tng::Services::ExtractMethods
47
+ include TTY::Option
48
+ include Tng::Utils
49
+
50
+ usage do
51
+ program "tng"
52
+ desc "LLM-Powered Rails Test Generator"
53
+ example "Interactive mode (method-specific test generation):", "bundle exec tng"
54
+ example " • Browse and search controllers/models/services", ""
55
+ example " • Select specific methods from filtered lists", ""
56
+ example " • Generate tests for individual methods", ""
57
+ end
58
+
59
+ option :type do
60
+ short "-t"
61
+ long "--type=TYPE"
62
+ desc "Test type (controller, model, service, other)"
63
+ permit %w[controller model service other c m mo s se o]
64
+ end
65
+
66
+ option :file do
67
+ short "-f"
68
+ long "--file=FILE"
69
+ desc "File name (without .rb extension)"
70
+ end
71
+
72
+ option :method do
73
+ short "-m"
74
+ long "--method=METHOD"
75
+ desc "Method name (for per-method tests)"
76
+ end
77
+
78
+ flag :help do
79
+ short "-h"
80
+ long "--help"
81
+ desc "Show help message"
82
+ end
83
+
84
+ def initialize
85
+ @prompt = TTY::Prompt.new
86
+ @pastel = Pastel.new
87
+ @terminal_width = begin
88
+ TTY::Screen.width
89
+ rescue StandardError
90
+ 80
91
+ end
92
+ @config_initialized = false
93
+ @http_client = nil
94
+ @testng = nil
95
+ end
96
+
97
+ def start
98
+ normalized_argv = preprocess_arguments(ARGV.dup)
99
+
100
+ parse(normalized_argv)
101
+
102
+ if params[:help]
103
+ help_display = ShowHelp.new(@pastel, Tng::VERSION)
104
+ puts help_display.render
105
+ return
106
+ end
107
+
108
+ rails_loaded = load_rails_environment
109
+
110
+ initialize_config_and_clients if rails_loaded
111
+
112
+ if params[:type] && params[:file]
113
+ run_direct_generation
114
+ else
115
+ if find_rails_root && !rails_loaded && !defined?(Rails)
116
+ puts @pastel.decorate("#{Tng::UI::Theme.icon(:error)} Failed to load Rails environment.", Tng::UI::Theme.color(:error))
117
+ puts @pastel.decorate("Please ensure you're running this command from a Rails application directory.", Tng::UI::Theme.color(:warning))
118
+ puts @pastel.decorate("Try running: bundle exec tng", Tng::UI::Theme.color(:muted))
119
+ return
120
+ end
121
+
122
+ return unless check_configuration
123
+
124
+ check_system_status
125
+ clear_screen
126
+ puts DisplayBanner.new(@pastel, Tng::VERSION).render
127
+ main_menu
128
+ end
129
+ end
130
+
131
+ def preprocess_arguments(argv)
132
+ normalized = []
133
+
134
+ argv.each do |arg|
135
+ case arg
136
+ when /^(type|t)=(.+)$/ # Handle type=value or t=value (without dash)
137
+ normalized << "--type=#{::Regexp.last_match(2)}"
138
+ when /^(file|f)=(.+)$/ # Handle file=value or f=value (without dash)
139
+ normalized << "--file=#{::Regexp.last_match(2)}"
140
+ when /^(method|m)=(.+)$/ # Handle method=value or m=value (without dash)
141
+ normalized << "--method=#{::Regexp.last_match(2)}"
142
+ when /^(help|h)=(.+)$/ # Handle help=value or h=value (without dash)
143
+ normalized << "--help=#{::Regexp.last_match(2)}"
144
+ when /^-([tf])=(.+)$/ # Handle -t=value or -f=value (convert to long form)
145
+ key = ::Regexp.last_match(1) == "t" ? "type" : "file"
146
+ normalized << "--#{key}=#{::Regexp.last_match(2)}"
147
+ when /^-([mh])=(.+)$/ # Handle -m=value or -h=value
148
+ key = ::Regexp.last_match(1) == "m" ? "method" : "help"
149
+ normalized << "--#{key}=#{::Regexp.last_match(2)}"
150
+ when /^--\w+=.+$/ # Handle --key=value (already correct)
151
+ normalized << arg
152
+ when /^-[tfmh]$/ # Handle single flags like -t, -f, -m, -h (keep as is)
153
+ normalized << arg
154
+ when /^--\w+$/ # Handle long flags like --help (keep as is)
155
+ normalized << arg
156
+ else
157
+ # Regular arguments or values
158
+ normalized << arg
159
+ end
160
+ end
161
+
162
+ normalized
163
+ end
164
+
165
+ def main_menu
166
+ loop do
167
+ clear_screen
168
+
169
+ puts DisplayBanner.new(@pastel, Tng::VERSION).render
170
+
171
+ begin
172
+ choice = @prompt.select(
173
+ center_text("Select an option:"),
174
+ cycle: true,
175
+ per_page: 5,
176
+ symbols: { marker: "→" }
177
+ ) do |menu|
178
+ menu.choice "Generate tests", :tests
179
+ menu.choice "User stats", :stats
180
+ menu.choice "About", :about
181
+ menu.choice "Exit", :exit, key: "q"
182
+ end
183
+
184
+ handle_choice(choice)
185
+ rescue TTY::Reader::InputInterrupt, Interrupt
186
+ choice = :exit
187
+ end
188
+
189
+ break if choice == :exit
190
+ end
191
+ end
192
+
193
+ def handle_choice(choice)
194
+ case choice
195
+ when :tests
196
+ handle_test_generation
197
+ when :stats
198
+ user_stats
199
+ when :about
200
+ show_about
201
+ when :exit
202
+ show_goodbye
203
+ end
204
+ end
205
+
206
+ def handle_test_generation
207
+ choice = @prompt.select(
208
+ center_text("What would you like to test?"),
209
+ cycle: true,
210
+ per_page: 5,
211
+ symbols: { marker: "→" }
212
+ ) do |menu|
213
+ menu.choice "Controller", :controller
214
+ menu.choice "Model", :model
215
+ menu.choice "Service", :service
216
+ menu.choice "Other", :other
217
+ menu.choice @pastel.decorate("#{Tng::UI::Theme.icon(:back)} Back", Tng::UI::Theme.color(:info)), :back
218
+ end
219
+
220
+ case choice
221
+ when :controller
222
+ generate_controller_tests
223
+ when :model
224
+ generate_model_tests
225
+ when :service
226
+ generate_service_tests
227
+ when :other
228
+ generate_other_tests
229
+ end
230
+ end
231
+
232
+ def generate_controller_tests
233
+ header = @pastel.decorate("Scanning for controllers...", Tng::UI::Theme.color(:primary))
234
+ puts center_text(header)
235
+
236
+ spinner = TTY::Spinner.new(
237
+ center_text("[:spinner] Analyzing controllers..."),
238
+ format: :dots,
239
+ success_mark: @pastel.decorate(Tng::UI::Theme.icon(:success), Tng::UI::Theme.color(:success)),
240
+ error_mark: @pastel.decorate(Tng::UI::Theme.icon(:error), Tng::UI::Theme.color(:error))
241
+ )
242
+ spinner.auto_spin
243
+ controllers = Tng::Analyzers::Controller.files_in_dir("app/controllers").map do |file|
244
+ relative_path = file[:path].gsub(%r{^.*app/controllers/}, "").gsub(".rb", "")
245
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
246
+
247
+ {
248
+ name: namespaced_name,
249
+ path: file[:path]
250
+ }
251
+ end
252
+
253
+ flow_display = ControllerTestFlowDisplay.new(@prompt, @pastel)
254
+
255
+ if controllers.empty?
256
+ spinner.error
257
+ flow_display.show_no_controllers_message
258
+ return
259
+ end
260
+
261
+ success_msg = @pastel.decorate("Found #{controllers.length} controllers", Tng::UI::Theme.color(:success))
262
+ spinner.success(center_text(success_msg))
263
+
264
+ controller_choice = flow_display.select_controller(controllers)
265
+ return if controller_choice == :back
266
+
267
+ show_controller_test_options(controller_choice)
268
+ end
269
+
270
+ def generate_model_tests
271
+ header = @pastel.decorate("Scanning for models...", Tng::UI::Theme.color(:primary))
272
+ puts center_text(header)
273
+
274
+ spinner = TTY::Spinner.new(
275
+ center_text("[:spinner] Analyzing models..."),
276
+ format: :dots,
277
+ success_mark: @pastel.decorate(Tng::UI::Theme.icon(:success), Tng::UI::Theme.color(:success)),
278
+ error_mark: @pastel.decorate(Tng::UI::Theme.icon(:error), Tng::UI::Theme.color(:error))
279
+ )
280
+ spinner.auto_spin
281
+
282
+ models = Tng::Analyzers::Model.files_in_dir("app/models").map do |file|
283
+ relative_path = file[:path].gsub(%r{^.*app/models/}, "").gsub(".rb", "")
284
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
285
+
286
+ {
287
+ name: namespaced_name,
288
+ path: file[:path]
289
+ }
290
+ end
291
+
292
+ flow_display = ModelTestFlowDisplay.new(@prompt, @pastel)
293
+
294
+ if models.empty?
295
+ spinner.error
296
+ flow_display.show_no_models_message
297
+ return
298
+ end
299
+
300
+ success_msg = @pastel.decorate("Found #{models.length} models", Tng::UI::Theme.color(:success))
301
+ spinner.success(center_text(success_msg))
302
+
303
+ model_choice = flow_display.select_model(models)
304
+ return if model_choice == :back
305
+
306
+ show_model_test_options(model_choice)
307
+ end
308
+
309
+ def generate_service_tests
310
+ header = @pastel.decorate("Scanning for services...", Tng::UI::Theme.color(:primary))
311
+ puts center_text(header)
312
+
313
+ spinner = TTY::Spinner.new(
314
+ center_text("[:spinner] Analyzing services..."),
315
+ format: :dots,
316
+ success_mark: @pastel.decorate(Tng::UI::Theme.icon(:success), Tng::UI::Theme.color(:success)),
317
+ error_mark: @pastel.decorate(Tng::UI::Theme.icon(:error), Tng::UI::Theme.color(:error))
318
+ )
319
+ spinner.auto_spin
320
+
321
+ services = Tng::Analyzers::Service.files_in_dir.map do |file|
322
+ relative_path = file[:path].gsub(%r{^.*app/services?/}, "").gsub(".rb", "")
323
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
324
+
325
+ {
326
+ name: namespaced_name,
327
+ path: file[:path]
328
+ }
329
+ end
330
+
331
+ flow_display = ServiceTestFlowDisplay.new(@prompt, @pastel)
332
+
333
+ if services.empty?
334
+ spinner.error
335
+ flow_display.show_no_services_message
336
+ return
337
+ end
338
+
339
+ success_msg = @pastel.decorate("Found #{services.length} services", Tng::UI::Theme.color(:success))
340
+ spinner.success(center_text(success_msg))
341
+
342
+ service_choice = flow_display.select_service(services)
343
+ return if service_choice == :back
344
+
345
+ show_service_test_options(service_choice)
346
+ end
347
+
348
+ def generate_other_tests
349
+ header = @pastel.decorate("Scanning for other files...", Tng::UI::Theme.color(:primary))
350
+ puts center_text(header)
351
+
352
+ spinner = TTY::Spinner.new(
353
+ center_text("[:spinner] Analyzing other files..."),
354
+ format: :dots,
355
+ success_mark: @pastel.decorate(Tng::UI::Theme.icon(:success), Tng::UI::Theme.color(:success)),
356
+ error_mark: @pastel.decorate(Tng::UI::Theme.icon(:error), Tng::UI::Theme.color(:error))
357
+ )
358
+ spinner.auto_spin
359
+
360
+ other_files = Tng::Analyzers::Other.files_in_dir.map do |file|
361
+ relative_path = file[:relative_path].gsub(".rb", "")
362
+ namespaced_name = relative_path.split("/").map(&:camelize).join("::")
363
+
364
+ {
365
+ name: namespaced_name,
366
+ path: file[:path],
367
+ type: file[:type]
368
+ }
369
+ end
370
+
371
+ flow_display = OtherTestFlowDisplay.new(@prompt, @pastel)
372
+
373
+ if other_files.empty?
374
+ spinner.error
375
+ flow_display.show_no_other_files_message
376
+ return
377
+ end
378
+
379
+ success_msg = @pastel.decorate("Found #{other_files.length} other files", Tng::UI::Theme.color(:success))
380
+ spinner.success(center_text(success_msg))
381
+
382
+ other_choice = flow_display.select_other_file(other_files)
383
+ return if other_choice == :back
384
+
385
+ show_other_test_options(other_choice)
386
+ end
387
+
388
+ def show_controller_method_selection(controller)
389
+ header = @pastel.decorate("#{Tng::UI::Theme.icon(:info)} Analyzing methods for #{controller[:name]}", Tng::UI::Theme.color(:primary))
390
+ puts center_text(header)
391
+
392
+ methods = extract_controller_methods(controller)
393
+ flow_display = ControllerTestFlowDisplay.new(@prompt, @pastel)
394
+
395
+ if methods.empty?
396
+ flow_display.show_no_methods_message(controller)
397
+ return
398
+ end
399
+
400
+ method_choice = flow_display.select_controller_method(controller, methods)
401
+
402
+ return if method_choice == :back
403
+
404
+ generate_test_for_controller_method(controller, method_choice)
405
+ end
406
+
407
+ def generate_test_for_controller_method(controller, method_info)
408
+ header = @pastel.decorate(
409
+ "#{Tng::UI::Theme.icon(:marker)} Generating test for #{controller[:name]}##{method_info[:name]}", Tng::UI::Theme.color(:primary)
410
+ )
411
+ puts center_text(header)
412
+
413
+ analysis_msg = @pastel.decorate("Analyzing method structure...", Tng::UI::Theme.color(:muted))
414
+ puts center_text(analysis_msg)
415
+
416
+ progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
417
+ progress_bar = TTY::ProgressBar.new(
418
+ "#{" " * progress_padding}[:bar] :percent :current/:total",
419
+ total: 5,
420
+ bar_format: :box,
421
+ complete: @pastel.decorate("█", Tng::UI::Theme.color(:success)),
422
+ incomplete: @pastel.decorate("░", Tng::UI::Theme.color(:muted)),
423
+ width: 40
424
+ )
425
+
426
+ step_msg = @pastel.decorate("Parsing method details...", Tng::UI::Theme.color(:muted))
427
+ puts center_text(step_msg)
428
+ progress_bar.advance(1)
429
+ sleep(0.3)
430
+
431
+ step_msg = @pastel.decorate("Analyzing method dependencies...", Tng::UI::Theme.color(:muted))
432
+ puts center_text(step_msg)
433
+ progress_bar.advance(1)
434
+ sleep(0.3)
435
+
436
+ step_msg = @pastel.decorate("Extracting method context...", Tng::UI::Theme.color(:muted))
437
+ puts center_text(step_msg)
438
+ progress_bar.advance(1)
439
+ sleep(0.3)
440
+
441
+ step_msg = @pastel.decorate("Generating method test code...", Tng::UI::Theme.color(:muted))
442
+ puts center_text(step_msg)
443
+ progress_bar.advance(1)
444
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_controller_method(controller, method_info)
445
+ sleep(0.5)
446
+
447
+ step_msg = @pastel.decorate("Writing test file...", Tng::UI::Theme.color(:muted))
448
+ puts center_text(step_msg)
449
+ progress_bar.advance(1)
450
+ sleep(0.2)
451
+
452
+ # Show completion message with test count and timing
453
+ if result && result[:test_count]
454
+ count_msg = result[:test_count] == 1 ? "1 test" : "#{result[:test_count]} tests"
455
+ time_msg = result[:generation_time] ? " in #{format_generation_time(result[:generation_time])}" : ""
456
+ completion_msg = @pastel.decorate(
457
+ "#{Tng::UI::Theme.icon(:success)} Generated #{count_msg} for controller method#{time_msg}!", Tng::UI::Theme.color(:success)
458
+ )
459
+ else
460
+ completion_msg = @pastel.decorate("#{Tng::UI::Theme.icon(:success)} Method test generation completed!", Tng::UI::Theme.color(:success))
461
+ end
462
+ puts center_text(completion_msg)
463
+
464
+ if result && result[:file_path]
465
+ show_post_generation_menu(result)
466
+ else
467
+ @prompt.keypress(center_text(@pastel.decorate("Press any key to continue...", Tng::UI::Theme.color(:muted))))
468
+ end
469
+ end
470
+
471
+ def show_controller_test_options(controller_choice)
472
+ return unless check_authentication_configuration
473
+
474
+ show_controller_method_selection(controller_choice)
475
+ end
476
+
477
+ def show_model_test_options(model_choice)
478
+ return unless check_authentication_configuration
479
+
480
+ show_model_method_selection(model_choice)
481
+ end
482
+
483
+ def show_service_test_options(service_choice)
484
+ return unless check_authentication_configuration
485
+
486
+ show_service_method_selection(service_choice)
487
+ end
488
+
489
+ def show_model_method_selection(model)
490
+ header = @pastel.decorate("#{Tng::UI::Theme.icon(:info)} Analyzing methods for #{model[:name]}", Tng::UI::Theme.color(:primary))
491
+ puts center_text(header)
492
+
493
+ methods = extract_model_methods(model)
494
+ flow_display = ModelTestFlowDisplay.new(@prompt, @pastel)
495
+
496
+ if methods.empty?
497
+ flow_display.show_no_methods_message(model)
498
+ return
499
+ end
500
+
501
+ method_choice = flow_display.select_model_method(model, methods)
502
+
503
+ return if method_choice == :back
504
+
505
+ generate_test_for_model_method(model, method_choice)
506
+ end
507
+
508
+ def generate_test_for_model_method(model, method_info)
509
+ header = @pastel.decorate(
510
+ "#{Tng::UI::Theme.icon(:marker)} Generating test for #{model[:name]}##{method_info[:name]}", Tng::UI::Theme.color(:primary)
511
+ )
512
+ puts center_text(header)
513
+
514
+ analysis_msg = @pastel.decorate("Analyzing method structure...", Tng::UI::Theme.color(:muted))
515
+ puts center_text(analysis_msg)
516
+
517
+ progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
518
+ progress_bar = TTY::ProgressBar.new(
519
+ "#{" " * progress_padding}[:bar] :percent :current/:total",
520
+ total: 4,
521
+ bar_format: :box,
522
+ complete: @pastel.decorate("█", Tng::UI::Theme.color(:success)),
523
+ incomplete: @pastel.decorate("░", Tng::UI::Theme.color(:muted)),
524
+ width: 40
525
+ )
526
+
527
+ step_msg = @pastel.decorate("Parsing method details...", Tng::UI::Theme.color(:muted))
528
+ puts center_text(step_msg)
529
+ progress_bar.advance(1)
530
+ sleep(0.3)
531
+
532
+ step_msg = @pastel.decorate("Analyzing method dependencies...", Tng::UI::Theme.color(:muted))
533
+ puts center_text(step_msg)
534
+ progress_bar.advance(1)
535
+ sleep(0.3)
536
+
537
+ step_msg = @pastel.decorate("Generating method test code...", Tng::UI::Theme.color(:muted))
538
+ puts center_text(step_msg)
539
+ progress_bar.advance(1)
540
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_model_method(model, method_info)
541
+ sleep(0.5)
542
+
543
+ step_msg = @pastel.decorate("Writing test file...", Tng::UI::Theme.color(:muted))
544
+ puts center_text(step_msg)
545
+ progress_bar.advance(1)
546
+ sleep(0.2)
547
+
548
+ # Show completion message with test count and timing
549
+ if result && result[:test_count]
550
+ count_msg = result[:test_count] == 1 ? "1 test" : "#{result[:test_count]} tests"
551
+ time_msg = result[:generation_time] ? " in #{format_generation_time(result[:generation_time])}" : ""
552
+ completion_msg = @pastel.decorate(
553
+ "#{Tng::UI::Theme.icon(:success)} Generated #{count_msg} for model method#{time_msg}!", Tng::UI::Theme.color(:success)
554
+ )
555
+ else
556
+ completion_msg = @pastel.decorate("#{Tng::UI::Theme.icon(:success)} Method test generation completed!", Tng::UI::Theme.color(:success))
557
+ end
558
+ puts center_text(completion_msg)
559
+
560
+ if result && result[:file_path]
561
+ show_post_generation_menu(result)
562
+ else
563
+ @prompt.keypress(center_text(@pastel.decorate("Press any key to continue...", Tng::UI::Theme.color(:muted))))
564
+ end
565
+ end
566
+
567
+ def generate_test_for_service_method(service, method_info)
568
+ header = @pastel.decorate(
569
+ "#{Tng::UI::Theme.icon(:marker)} Generating test for #{service[:name]}##{method_info[:name]}", Tng::UI::Theme.color(:primary)
570
+ )
571
+ puts center_text(header)
572
+
573
+ analysis_msg = @pastel.decorate("Analyzing method structure...", Tng::UI::Theme.color(:muted))
574
+ puts center_text(analysis_msg)
575
+
576
+ progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
577
+ progress_bar = TTY::ProgressBar.new(
578
+ "#{" " * progress_padding}[:bar] :percent :current/:total",
579
+ total: 4,
580
+ bar_format: :box,
581
+ complete: @pastel.decorate("█", Tng::UI::Theme.color(:success)),
582
+ incomplete: @pastel.decorate("░", Tng::UI::Theme.color(:muted)),
583
+ width: 40
584
+ )
585
+
586
+ step_msg = @pastel.decorate("Parsing method details...", Tng::UI::Theme.color(:muted))
587
+ puts center_text(step_msg)
588
+ progress_bar.advance(1)
589
+ sleep(0.3)
590
+
591
+ step_msg = @pastel.decorate("Analyzing method dependencies...", Tng::UI::Theme.color(:muted))
592
+ puts center_text(step_msg)
593
+ progress_bar.advance(1)
594
+ sleep(0.3)
595
+
596
+ step_msg = @pastel.decorate("Generating method test code...", Tng::UI::Theme.color(:muted))
597
+ puts center_text(step_msg)
598
+ progress_bar.advance(1)
599
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_service_method(service, method_info)
600
+ sleep(0.5)
601
+
602
+ step_msg = @pastel.decorate("Writing test file...", Tng::UI::Theme.color(:muted))
603
+ puts center_text(step_msg)
604
+ progress_bar.advance(1)
605
+ sleep(0.2)
606
+
607
+ # Show completion message with test count and timing
608
+ if result && result[:test_count]
609
+ count_msg = result[:test_count] == 1 ? "1 test" : "#{result[:test_count]} tests"
610
+ time_msg = result[:generation_time] ? " in #{format_generation_time(result[:generation_time])}" : ""
611
+ completion_msg = @pastel.decorate(
612
+ "#{Tng::UI::Theme.icon(:success)} Generated #{count_msg} for service method#{time_msg}!", Tng::UI::Theme.color(:success)
613
+ )
614
+ else
615
+ completion_msg = @pastel.decorate("#{Tng::UI::Theme.icon(:success)} Method test generation completed!", Tng::UI::Theme.color(:success))
616
+ end
617
+ puts center_text(completion_msg)
618
+
619
+ if result && result[:file_path]
620
+ show_post_generation_menu(result)
621
+ else
622
+ @prompt.keypress(center_text(@pastel.decorate("Press any key to continue...", Tng::UI::Theme.color(:muted))))
623
+ end
624
+ end
625
+
626
+ def show_service_method_selection(service)
627
+ header = @pastel.decorate("#{Tng::UI::Theme.icon(:info)} Analyzing methods for #{service[:name]}", Tng::UI::Theme.color(:primary))
628
+ puts center_text(header)
629
+
630
+ methods = extract_service_methods(service)
631
+ flow_display = ServiceTestFlowDisplay.new(@prompt, @pastel)
632
+
633
+ if methods.empty?
634
+ flow_display.show_no_methods_message(service)
635
+ return
636
+ end
637
+
638
+ method_choice = flow_display.select_service_method(service, methods)
639
+
640
+ return if method_choice == :back
641
+
642
+ generate_test_for_service_method(service, method_choice)
643
+ end
644
+
645
+ def show_other_test_options(other_choice)
646
+ return unless check_authentication_configuration
647
+
648
+ show_other_method_selection(other_choice)
649
+ end
650
+
651
+ def show_other_method_selection(other_file)
652
+ header = @pastel.decorate("#{Tng::UI::Theme.icon(:info)} Analyzing methods for #{other_file[:name]}", Tng::UI::Theme.color(:primary))
653
+ puts center_text(header)
654
+
655
+ methods = extract_other_methods(other_file)
656
+ flow_display = OtherTestFlowDisplay.new(@prompt, @pastel)
657
+
658
+ if methods.empty?
659
+ flow_display.show_no_methods_message(other_file)
660
+ return
661
+ end
662
+
663
+ method_choice = flow_display.select_other_method(other_file, methods)
664
+
665
+ return if method_choice == :back
666
+
667
+ generate_test_for_other_method(other_file, method_choice)
668
+ end
669
+
670
+ def generate_test_for_other_method(other_file, method_info)
671
+ header = @pastel.decorate(
672
+ "#{Tng::UI::Theme.icon(:marker)} Generating test for #{other_file[:name]}##{method_info[:name]}", Tng::UI::Theme.color(:primary)
673
+ )
674
+ puts center_text(header)
675
+
676
+ analysis_msg = @pastel.decorate("Analyzing method structure...", Tng::UI::Theme.color(:muted))
677
+ puts center_text(analysis_msg)
678
+
679
+ progress_padding = [(@terminal_width - 56) / 2, 0].max # Account for progress bar width
680
+ progress_bar = TTY::ProgressBar.new(
681
+ "#{" " * progress_padding}[:bar] :percent :current/:total",
682
+ total: 4,
683
+ bar_format: :box,
684
+ complete: @pastel.decorate("█", Tng::UI::Theme.color(:success)),
685
+ incomplete: @pastel.decorate("░", Tng::UI::Theme.color(:muted)),
686
+ width: 40
687
+ )
688
+
689
+ step_msg = @pastel.decorate("Parsing method details...", Tng::UI::Theme.color(:muted))
690
+ puts center_text(step_msg)
691
+ progress_bar.advance(1)
692
+ sleep(0.3)
693
+
694
+ step_msg = @pastel.decorate("Analyzing method dependencies...", Tng::UI::Theme.color(:muted))
695
+ puts center_text(step_msg)
696
+ progress_bar.advance(1)
697
+ sleep(0.3)
698
+
699
+ step_msg = @pastel.decorate("Generating method test code...", Tng::UI::Theme.color(:muted))
700
+ puts center_text(step_msg)
701
+ progress_bar.advance(1)
702
+ result = Tng::Services::TestGenerator.new(@http_client).run_for_other_method(other_file, method_info)
703
+ sleep(0.5)
704
+
705
+ step_msg = @pastel.decorate("Writing test file...", Tng::UI::Theme.color(:muted))
706
+ puts center_text(step_msg)
707
+ progress_bar.advance(1)
708
+ sleep(0.2)
709
+
710
+ # Show completion message with test count and timing
711
+ if result && result[:test_count]
712
+ count_msg = result[:test_count] == 1 ? "1 test" : "#{result[:test_count]} tests"
713
+ time_msg = result[:generation_time] ? " in #{format_generation_time(result[:generation_time])}" : ""
714
+ completion_msg = @pastel.decorate(
715
+ "#{Tng::UI::Theme.icon(:success)} Generated #{count_msg} for other method#{time_msg}!", Tng::UI::Theme.color(:success)
716
+ )
717
+ else
718
+ completion_msg = @pastel.decorate("#{Tng::UI::Theme.icon(:success)} Method test generation completed!", Tng::UI::Theme.color(:success))
719
+ end
720
+ puts center_text(completion_msg)
721
+
722
+ if result && result[:file_path]
723
+ show_post_generation_menu(result)
724
+ else
725
+ @prompt.keypress(center_text(@pastel.decorate("Press any key to continue...", Tng::UI::Theme.color(:muted))))
726
+ end
727
+ end
728
+
729
+ # STATS
730
+ def user_stats
731
+ stats_data = @testng.get_user_stats
732
+ UserStatsDisplay.new(@pastel, @prompt).display(stats_data)
733
+ end
734
+
735
+ def check_system_status
736
+ unless params[:type] && params[:file]
737
+ puts @pastel.decorate("#{Tng::UI::Theme.icon(:info)} Checking system status...",
738
+ Tng::UI::Theme.color(:muted))
739
+ end
740
+
741
+ status = @testng.check_system_status
742
+ status_ok = SystemStatusDisplay.new(@pastel, params).display(status)
743
+
744
+ return if status_ok
745
+
746
+ puts
747
+ puts @pastel.decorate("Press any key to exit...", Tng::UI::Theme.color(:muted))
748
+ $stdin.getch
749
+ exit(1)
750
+ end
751
+
752
+ def check_configuration
753
+ return true if Tng::Services::UserAppConfig.configured?
754
+
755
+ missing = Tng::Services::UserAppConfig.missing_config
756
+
757
+ ConfigurationDisplay.new(@pastel).display_missing_config(missing)
758
+ false
759
+ end
760
+
761
+ def check_authentication_configuration
762
+ auth_warning = AuthenticationWarningDisplay.new(@pastel)
763
+ auth_missing = auth_warning.display_if_missing
764
+
765
+ return false if auth_missing
766
+
767
+ true
768
+ end
769
+
770
+ # ABOUT
771
+ def show_about
772
+ AboutDisplay.new(@pastel, @prompt, Tng::VERSION).display
773
+ end
774
+
775
+ # GOODBYE
776
+ def show_goodbye
777
+ clear_screen
778
+ GoodbyeDisplay.new(@pastel).display
779
+ end
780
+
781
+ def run_direct_generation
782
+ return unless check_configuration
783
+
784
+ return unless check_authentication_configuration
785
+
786
+ status = @testng.check_system_status
787
+ unless SystemStatusDisplay.new(@pastel, params).display(status)
788
+ puts @pastel.red("❌ System status check failed")
789
+ return
790
+ end
791
+
792
+ direct_generator = Tng::Services::DirectGeneration.new(
793
+ @pastel,
794
+ @testng,
795
+ @http_client,
796
+ params,
797
+ method(:show_post_generation_menu)
798
+ )
799
+
800
+ direct_generator.run
801
+ end
802
+
803
+ def show_post_generation_menu(result)
804
+ is_direct_mode = params[:type] && params[:file]
805
+
806
+ if is_direct_mode
807
+ puts
808
+ header = @pastel.decorate("#{Tng::UI::Theme.icon(:rocket)} Test Generated Successfully!", Tng::UI::Theme.color(:primary))
809
+ puts center_text(header)
810
+ puts
811
+
812
+ info_msg = [
813
+ @pastel.decorate("#{Tng::UI::Theme.icon(:config)} File: #{result[:file_path]}", Tng::UI::Theme.color(:info)),
814
+ @pastel.decorate("#{Tng::UI::Theme.icon(:marker)} Run: #{result[:run_command]}", Tng::UI::Theme.color(:warning))
815
+ ].join("\n")
816
+ puts center_text(info_msg)
817
+ puts
818
+
819
+ puts center_text(@pastel.decorate("#{Tng::UI::Theme.icon(:success)} Test generation completed successfully!", Tng::UI::Theme.color(:success)))
820
+ return
821
+ end
822
+
823
+ loop do
824
+ puts
825
+ header = @pastel.decorate("#{Tng::UI::Theme.icon(:rocket)} Test Generated Successfully!", Tng::UI::Theme.color(:primary))
826
+ puts center_text(header)
827
+ puts
828
+
829
+ info_msg = [
830
+ @pastel.decorate("#{Tng::UI::Theme.icon(:config)} File: #{result[:file_path]}", Tng::UI::Theme.color(:info)),
831
+ @pastel.decorate("#{Tng::UI::Theme.icon(:marker)} Run: #{result[:run_command]}", Tng::UI::Theme.color(:warning))
832
+ ].join("\n")
833
+ puts center_text(info_msg)
834
+ puts
835
+
836
+ choice = @prompt.select(
837
+ @pastel.decorate("What would you like to do?", Tng::UI::Theme.color(:primary)),
838
+ cycle: true,
839
+ symbols: { marker: Tng::UI::Theme.icon(:marker) }
840
+ ) do |menu|
841
+ menu.choice @pastel.decorate("#{Tng::UI::Theme.icon(:config)} Copy run command", Tng::UI::Theme.color(:warning)),
842
+ :copy_command
843
+ menu.choice @pastel.decorate("#{Tng::UI::Theme.icon(:back)} Back to main menu", Tng::UI::Theme.color(:info)),
844
+ :back
845
+ end
846
+
847
+ case choice
848
+ when :copy_command
849
+ copy_to_clipboard(result[:run_command])
850
+ when :back
851
+ break
852
+ end
853
+ end
854
+ end
855
+
856
+ def initialize_config_and_clients
857
+ @config_initialized = true
858
+
859
+ unless Tng::Services::UserAppConfig.configured?
860
+ missing = Tng::Services::UserAppConfig.missing_config
861
+ ConfigurationDisplay.new(@pastel).display_missing_config(missing)
862
+ exit 1
863
+ end
864
+
865
+ @http_client = Tng::HttpClient.new(
866
+ Tng::Services::UserAppConfig.base_url,
867
+ Tng::Services::UserAppConfig.api_key
868
+ )
869
+ @testng = Services::Testng.new(@http_client)
870
+ end
871
+
872
+ private
873
+
874
+ def format_generation_time(seconds)
875
+ if seconds < 1
876
+ "#{(seconds * 1000).round}ms"
877
+ elsif seconds < 60
878
+ "#{seconds.round(1)}s"
879
+ else
880
+ minutes = (seconds / 60).floor
881
+ remaining_seconds = (seconds % 60).round
882
+ "#{minutes}m #{remaining_seconds}s"
883
+ end
884
+ end
885
+ end
886
+
887
+ cli = CLI.new
888
+ cli.start