tng 0.3.0 → 0.3.3

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.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -70,6 +70,25 @@ module Tng
70
70
  response
71
71
  end
72
72
 
73
+ # Verify if the API key is valid
74
+ # Returns: { valid: true/false, error: nil/String }
75
+ def verify_auth
76
+ response = get("cli/tng_rails/stats")
77
+
78
+ return { valid: false, error: "Network error" } if response.is_a?(HTTPX::ErrorResponse)
79
+
80
+ case response.status
81
+ when 200, 201
82
+ { valid: true, error: nil }
83
+ when 401
84
+ { valid: false, error: "Invalid or missing API key" }
85
+ when 403
86
+ { valid: false, error: "API key expired or usage limit reached" }
87
+ else
88
+ { valid: false, error: "Unexpected error (#{response.status})" }
89
+ end
90
+ end
91
+
73
92
  private
74
93
 
75
94
  def stream_default_headers
@@ -20,53 +20,49 @@ module Tng
20
20
  @pastel = Pastel.new
21
21
  end
22
22
 
23
- def run_for_controller_method(controller, method_info)
24
- generate_test_for_type(controller, method_info, :controller)
23
+ def run_for_controller_method(controller, method_info, progress: nil)
24
+ generate_test_for_type(controller, method_info, :controller, progress: progress)
25
25
  end
26
26
 
27
- def run_for_model_method(model, method_info)
28
- generate_test_for_type(model, method_info, :model)
27
+ def run_for_model_method(model, method_info, progress: nil)
28
+ generate_test_for_type(model, method_info, :model, progress: progress)
29
29
  end
30
30
 
31
- def run_for_service_method(service, method_info)
32
- generate_test_for_type(service, method_info, :service)
31
+ def run_for_service_method(service, method_info, progress: nil)
32
+ generate_test_for_type(service, method_info, :service, progress: progress)
33
33
  end
34
34
 
35
- def run_for_other_method(other_file, method_info)
36
- generate_test_for_type(other_file, method_info, :other)
35
+ def run_for_other_method(other_file, method_info, progress: nil)
36
+ generate_test_for_type(other_file, method_info, :other, progress: progress)
37
37
  end
38
38
 
39
- private
40
39
 
41
- def generate_test_for_type(file_object, method_info, type)
40
+ def generate_test_for_type(file_object, method_info, type, progress: nil)
42
41
  start_time = Time.now
43
42
 
44
43
  response = send_request_for_type(file_object, method_info, type)
45
44
  return unless response
46
45
 
47
46
  if response.is_a?(HTTPX::ErrorResponse)
48
- error_icon = Tng::UI::Theme.icon(:error)
49
- error_color = Tng::UI::Theme.color(:error)
50
- puts "#{error_icon} #{@pastel.decorate("Request failed:",
51
- error_color)} #{response.error&.message || "Unknown error"}"
52
- return
47
+ return { error: :network_error, message: response.error&.message || "Network error" }
53
48
  end
54
49
 
55
50
  job_data = JSON.parse(response.body)
56
51
 
52
+ # Check for authentication errors
53
+ if response.status == 401
54
+ return { error: :auth_failed, message: "Invalid or missing API key" }
55
+ end
56
+
57
57
  # Check for forbidden responses (usage limits or invalid keys)
58
58
  if response.status == 403
59
- error_icon = Tng::UI::Theme.icon(:error)
60
- error_color = Tng::UI::Theme.color(:error)
61
- puts "#{error_icon} #{@pastel.decorate("Access denied:",
62
- error_color)} Please check your API key and usage limits."
63
- return
59
+ return { error: :auth_failed, message: "API key expired or usage limit reached" }
64
60
  end
65
61
  job_id = job_data["job_id"]
66
62
 
67
63
  return unless job_id
68
64
 
69
- result = poll_for_completion(job_id)
65
+ result = poll_for_completion(job_id, progress: progress)
70
66
 
71
67
  return unless result
72
68
 
@@ -106,21 +102,57 @@ module Tng
106
102
  ]
107
103
  end
108
104
 
109
- def poll_for_completion(job_id)
105
+ def poll_for_completion(job_id, progress: nil)
110
106
  start_time = Time.current
111
- progress_bar = create_progress_bar
107
+ attempts = 0
108
+ max_attempts = MAX_POLL_DURATION_SECONDS / POLL_INTERVAL_SECONDS
109
+
110
+ # Initialize agent steps if progress tracking is enabled
111
+ agent_step_indices = {}
112
+ if progress
113
+ # Current step index in progress updater (assuming it follows previous steps)
114
+ # We create 4 generic steps for the agents
115
+ # Note: indices are relative to the current session, so we just append them.
116
+ # But we need their indices to update them later.
117
+ # Since we can't ask progress for current index easily without hacking,
118
+ # we rely on the fact that we call update 4 times.
119
+
120
+ # Use a base offset if we could know it, but we can't reliably.
121
+ # Actually, if we use explicit_step, we need absolute indices.
122
+ # Let's assume the previous steps were 0, 1, 2, 3 based on bin/tng.
123
+ # So we start at 4.
124
+ base_idx = 4
125
+
126
+ progress.update("Context Builder: Pending...", nil, step_increment: true)
127
+ agent_step_indices["context_agent_status"] = base_idx
128
+
129
+ progress.update("Style Analyzer: Pending...", nil, step_increment: true)
130
+ agent_step_indices["style_agent_status"] = base_idx + 1
131
+
132
+ progress.update("Logic Analyzer: Pending...", nil, step_increment: true)
133
+ agent_step_indices["logical_issue_status"] = base_idx + 2
134
+
135
+ progress.update("Logic Generator: Pending...", nil, step_increment: true)
136
+ agent_step_indices["behavior_expert_status"] = base_idx + 3
137
+ end
112
138
 
113
139
  loop do
140
+ attempts += 1
114
141
  seconds_elapsed = (Time.current - start_time).to_i
115
142
 
116
143
  if seconds_elapsed > MAX_POLL_DURATION_SECONDS
117
- progress_bar.finish
118
- puts "❌ Test generation timed out after #{MAX_POLL_DURATION_SECONDS} seconds (7 minutes)"
119
- return
144
+ return { error: :timeout, message: "Test generation timed out after #{MAX_POLL_DURATION_SECONDS} seconds" }
120
145
  end
121
146
 
122
- status_text = determine_status_text(seconds_elapsed)
123
- progress_bar.advance(1, status: status_text)
147
+ # Calculate pseudo-percentage
148
+ 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.
153
+ else
154
+ pct = 0
155
+ end
124
156
 
125
157
  sleep(POLL_INTERVAL_SECONDS)
126
158
 
@@ -128,31 +160,73 @@ module Tng
128
160
 
129
161
  if status_response.is_a?(HTTPX::ErrorResponse)
130
162
  debug_log("Status check failed: #{status_response.error&.message}") if debug_enabled?
131
- next # Continue polling on network errors
163
+ next
132
164
  end
133
165
 
134
166
  begin
135
167
  status_data = JSON.parse(status_response.body.to_s)
136
168
  status = status_data["status"]
169
+
170
+ # Update UI with granular info
171
+ if progress && status_data["info"].is_a?(Hash)
172
+ info = status_data["info"]
173
+
174
+ 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
195
+
196
+ msg = if agent_status == "processing"
197
+ "#{label}: Processing..."
198
+ elsif agent_status == "completed"
199
+ "#{label}: Completed"
200
+ elsif agent_status == "failed"
201
+ "#{label}: Failed"
202
+ else
203
+ "#{label}: #{agent_status.capitalize}..."
204
+ end
205
+
206
+ if values.any?
207
+ # Clean values
208
+ clean_vals = values.map { |v| v.to_s.gsub("_", " ").gsub("'", "").gsub(":", "").strip }
209
+ display_str = clean_vals.first(3).join(", ")
210
+ display_str += ", ..." if clean_vals.size > 3
211
+ msg += " (#{display_str})"
212
+ end
213
+
214
+ # Pass percentage only on the last step (Logic Generator) to keep main bar moving?
215
+ # Or pass it on all updates.
216
+ p = (step_idx == agent_step_indices["behavior_expert_status"]) ? pct : nil
217
+
218
+ progress.update(msg, p, step_increment: false, explicit_step: step_idx)
219
+ end
220
+ end
137
221
 
138
222
  case status
139
223
  when "completed"
140
224
  debug_log("Test generation completed!") if debug_enabled?
141
- progress_bar.finish
142
- success_icon = Tng::UI::Theme.icon(:success)
143
- success_color = Tng::UI::Theme.color(:success)
144
- puts "#{success_icon} #{@pastel.decorate("Test generation completed!", success_color)}"
145
225
  return status_data["result"]
146
226
  when "failed"
147
227
  debug_log("Test generation failed: #{status_data["error"]}") if debug_enabled?
148
- progress_bar.finish
149
- error_icon = Tng::UI::Theme.icon(:error)
150
- error_color = Tng::UI::Theme.color(:error)
151
- puts "#{error_icon} #{@pastel.decorate("Test generation failed:",
152
- error_color)} An error occurred while generating tests. Please try again."
153
- return
228
+ return { error: :generation_failed, message: status_data["error"] }
154
229
  when "pending", "processing"
155
- # Progress bar updates smoothly above
156
230
  next
157
231
  else
158
232
  debug_log("Unknown test generation status: #{status}") if debug_enabled?
@@ -165,47 +239,6 @@ module Tng
165
239
  end
166
240
  end
167
241
 
168
- def create_progress_bar
169
- rocket_icon = Tng::UI::Theme.icon(:rocket)
170
- success_color = Tng::UI::Theme.color(:success)
171
- primary_color = Tng::UI::Theme.color(:primary)
172
- muted_color = Tng::UI::Theme.color(:muted)
173
-
174
- complete_char = @pastel.decorate("\u2593", success_color)
175
- incomplete_char = @pastel.decorate("\u2591", muted_color)
176
- head_char = @pastel.decorate("\u25B6", success_color)
177
-
178
- TTY::ProgressBar.new(
179
- "#{rocket_icon} #{@pastel.decorate("Generating tests",
180
- primary_color)} #{@pastel.decorate("[:bar]",
181
- :white)} #{@pastel.decorate(":status",
182
- muted_color)} #{@pastel.decorate(
183
- "(:elapsed)", muted_color
184
- )}",
185
- total: nil,
186
- complete: complete_char,
187
- incomplete: incomplete_char,
188
- head: head_char,
189
- width: 40,
190
- clear: true,
191
- frequency: 10, # Update 10 times per second for smooth animation
192
- interval: 1 # Show elapsed time updates every second
193
- )
194
- end
195
-
196
- def determine_status_text(seconds_elapsed)
197
- case seconds_elapsed
198
- when 0..15 then "initializing..."
199
- when 16..45 then "analyzing code structure..."
200
- when 46..90 then "generating test cases..."
201
- when 91..150 then "optimizing test logic..."
202
- when 151..210 then "refining assertions..."
203
- when 211..270 then "formatting output..."
204
- when 271..330 then "finalizing tests..."
205
- else "completing generation..."
206
- end
207
- end
208
-
209
242
  def trigger_cleanup(job_id)
210
243
  @http_client.patch("#{CONTENT_RESPONSES_PATH}/#{job_id}/cleanup")
211
244
  rescue StandardError => e
@@ -84,16 +84,10 @@ module Services
84
84
 
85
85
  if response.status == 200
86
86
  begin
87
- stats_data = JSON.parse(response.body)
88
- puts "✅ Statistics fetched successfully!"
89
- stats_data
90
- rescue JSON::ParserError => e
91
- puts "❌ Failed to parse statistics response: #{e.message}"
87
+ JSON.parse(response.body)
88
+ rescue JSON::ParserError
92
89
  nil
93
90
  end
94
- else
95
- puts "❌ Failed to fetch statistics (#{response.status})"
96
- nil
97
91
  end
98
92
  end
99
93
  end