Class: Tng::Services::TestGenerator

Inherits:
Object
  • Object
show all
Defined in:
lib/tng/services/test_generator.rb

Constant Summary collapse

GENERATE_TESTS_PATH =
"cli/tng_rails/contents/generate_tests"
CONTENT_RESPONSES_PATH =
"cli/tng_rails/content_responses"
RUBY_CONTENT_RESPONSES_PATH =
"cli/tng_ruby/content_responses"
POLL_INTERVAL_SECONDS =

Poll every 5 seconds

5
MAX_POLL_DURATION_SECONDS =

7 minutes total

420

Instance Method Summary collapse

Constructor Details

#initialize(http_client) ⇒ TestGenerator

Returns a new instance of TestGenerator.



18
19
20
21
22
# File 'lib/tng/services/test_generator.rb', line 18

def initialize(http_client)
  @http_client = http_client
  @machine_info = Tng.machine_info
  @pastel = Pastel.new
end

Instance Method Details

#debug_enabled?Boolean

Returns:

  • (Boolean)


414
415
416
# File 'lib/tng/services/test_generator.rb', line 414

def debug_enabled?
  ENV["DEBUG"] == "1"
end

#debug_log(message) ⇒ Object



418
419
420
# File 'lib/tng/services/test_generator.rb', line 418

def debug_log(message)
  puts "-> DEBUG [TestGenerator]: #{message}"
end

#enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode:) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/tng/services/test_generator.rb', line 232

def enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode:)
  return method_info unless method_info.is_a?(Hash)
  return method_info if method_info.key?(:trace_v2) || method_info.key?("trace_v2")

  method_name = method_info[:name] || method_info["name"]
  return method_info if method_name.to_s.empty?

  class_name = method_info[:class_name] || method_info["class_name"]
  project_root = Dir.pwd
  path = File.expand_path(file_object[:path])

  trace_v2 =
    if ruby_mode
      Tng::Analyzer::Context.analyze_symbolic_trace_ruby_v2(project_root, path, method_name, class_name)
    else
      Tng::Analyzer::Context.analyze_symbolic_trace_v2(project_root, path, method_name, class_name)
    end

  method_info.merge(trace_v2: trace_v2)
rescue StandardError => e
  debug_log("Trace v2 enrichment failed for #{file_object[:path]}##{method_name}: #{e.message}") if debug_enabled?
  method_info
end

#generate_test_for_ruby(file_object, method_info, progress: nil) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/tng/services/test_generator.rb', line 167

def generate_test_for_ruby(file_object, method_info, progress: nil)
  start_time = Time.now

  response = send_request_for_ruby(file_object, method_info, audit_mode: false)
  return unless response

  if response.is_a?(HTTPX::ErrorResponse)
    return { error: :network_error, message: response.error&.message || "Network error" }
  end

  job_data = JSON.parse(response.body)
  return { error: :auth_failed, message: "Invalid or missing API key" } if response.status == 401
  return { error: :auth_failed, message: "API key expired or usage limit reached" } if response.status == 403

  job_id = job_data["job_id"]
  return unless job_id

  result = poll_for_completion(job_id, progress: progress, content_path: RUBY_CONTENT_RESPONSES_PATH)
  return unless result

  end_time = Time.now
  generation_time = end_time - start_time

  file_result = Tng::Utils.save_test_file(result.to_json)
  return unless file_result

  trigger_cleanup(job_id, content_path: RUBY_CONTENT_RESPONSES_PATH)

  file_result.merge(generation_time: generation_time)
end

#generate_test_for_type(file_object, method_info, type, progress: nil) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/tng/services/test_generator.rb', line 112

def generate_test_for_type(file_object, method_info, type, progress: nil)
  start_time = Time.now

  response = send_request_for_type(file_object, method_info, type)
  return unless response

  if response.is_a?(HTTPX::ErrorResponse)
    return { error: :network_error, message: response.error&.message || "Network error" }
  end

  job_data = JSON.parse(response.body)

  # Check for authentication errors
  return { error: :auth_failed, message: "Invalid or missing API key" } if response.status == 401

  # Check for forbidden responses (usage limits or invalid keys)
  return { error: :auth_failed, message: "API key expired or usage limit reached" } if response.status == 403

  job_id = job_data["job_id"]

  return unless job_id

  result = poll_for_completion(job_id, progress: progress, content_path: CONTENT_RESPONSES_PATH)

  return unless result

  end_time = Time.now
  generation_time = end_time - start_time

  file_result = Tng::Utils.save_test_file(result.to_json)
  return unless file_result

  trigger_cleanup(job_id, content_path: CONTENT_RESPONSES_PATH)

  file_result.merge(generation_time: generation_time)
end

#init_agent_steps(progress) ⇒ Object



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/tng/services/test_generator.rb', line 344

def init_agent_steps(progress)
  return {} unless progress

  base_idx = 4
  progress.update("Context Builder: Pending...", nil, step_increment: true)
  progress.update("Style Analyzer: Pending...", nil, step_increment: true)
  progress.update("Logic Analyzer: Pending...", nil, step_increment: true)
  progress.update("Logic Generator: Pending...", nil, step_increment: true)

  {
    "context_agent_status" => base_idx,
    "style_agent_status" => base_idx + 1,
    "logical_issue_status" => base_idx + 2,
    "behavior_expert_status" => base_idx + 3
  }
end

#poll_for_completion(job_id, progress: nil, content_path: CONTENT_RESPONSES_PATH) ⇒ Object

Modified method signature and added interrupt handler



266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/tng/services/test_generator.rb', line 266

def poll_for_completion(job_id, progress: nil, content_path: CONTENT_RESPONSES_PATH)
  start_time = Time.now
  attempts = 0
  max_attempts = MAX_POLL_DURATION_SECONDS / POLL_INTERVAL_SECONDS

  # Initialize agent steps if progress tracking is enabled
  agent_step_indices = init_agent_steps(progress)

  loop do
    attempts += 1
    seconds_elapsed = (Time.now - start_time).to_i

    if seconds_elapsed > MAX_POLL_DURATION_SECONDS
      return { error: :timeout, message: "Test generation timed out after #{MAX_POLL_DURATION_SECONDS} seconds" }
    end

    # Calculate pseudo-percentage
    if progress
      pct = ((attempts.to_f / max_attempts.to_f) * 95.0).to_i
    # Update global progress bar without adding a new step (explicit_step=4 updates Context Builder line, but we want to just update percentage?)
    # The progress bar component takes the percent from the *last* update message.
    # So we can pass specific percent when we update any agent step.
    else
      pct = 0
    end

    sleep(POLL_INTERVAL_SECONDS)

    status_response = @http_client.get("#{content_path}/#{job_id}")

    if status_response.is_a?(HTTPX::ErrorResponse)
      debug_log("Status check failed: #{status_response.error&.message}") if debug_enabled?
      next
    end

    begin
      status_data = JSON.parse(status_response.body.to_s, symbolize_names: true)
      status = status_data[:status]

      # Update UI with granular info
      update_progress_from_info(progress, agent_step_indices, status_data[:info], pct) if progress

      case status
      when "completed"
        debug_log("Test generation completed!") if debug_enabled?
        return status_data[:result].merge(analysis: status_data[:analysis])
      when "failed"
        debug_log("Test generation failed: #{status_data[:error]}") if debug_enabled?
        return { error: :generation_failed, message: status_data[:error] }
      when "pending", "processing"
        next
      else
        debug_log("Unknown test generation status: #{status}") if debug_enabled?
        next
      end
    rescue JSON::ParserError => e
      debug_log("JSON parse error: #{e.message}") if debug_enabled?
      next
    end
  end
rescue Interrupt
  # User pressed Ctrl+C - exit gracefully
  system("clear") || system("cls")
  puts "\n\n#{TTY::Box.frame(
    "Generation cancelled by user",
    padding: 1,
    align: :center,
    border: :thick,
    style: {
      fg: :yellow,
      border: {
        fg: :yellow
      }
    }
  )}"
  exit(0)
end

#request_configObject



256
257
258
259
260
261
262
263
# File 'lib/tng/services/test_generator.rb', line 256

def request_config
  [
    Tng::Utils.fixture_content,
    Tng::Services::UserAppConfig.config_with_source,
    Tng::Services::UserAppConfig.base_url,
    Tng::Services::UserAppConfig.api_key
  ]
end

#run_audit_for_controller_method(controller, method_info, progress: nil) ⇒ Object

Audit mode - async, polls for completion



46
47
48
# File 'lib/tng/services/test_generator.rb', line 46

def run_audit_for_controller_method(controller, method_info, progress: nil)
  run_audit_for_type(controller, method_info, :controller, progress: progress)
end

#run_audit_for_model_method(model, method_info, progress: nil) ⇒ Object



50
51
52
# File 'lib/tng/services/test_generator.rb', line 50

def run_audit_for_model_method(model, method_info, progress: nil)
  run_audit_for_type(model, method_info, :model, progress: progress)
end

#run_audit_for_other_method(other_file, method_info, progress: nil) ⇒ Object



58
59
60
# File 'lib/tng/services/test_generator.rb', line 58

def run_audit_for_other_method(other_file, method_info, progress: nil)
  run_audit_for_type(other_file, method_info, :other, progress: progress)
end

#run_audit_for_ruby(file_object, method_info, progress: nil) ⇒ Object



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/tng/services/test_generator.rb', line 198

def run_audit_for_ruby(file_object, method_info, progress: nil)
  response = send_request_for_ruby(file_object, method_info, audit_mode: true)
  return { error: :network_error, message: "Network error" } unless response

  if response.is_a?(HTTPX::ErrorResponse)
    return { error: :network_error, message: response.error&.message || "Network error" }
  end

  return { error: :auth_failed, message: "Invalid or missing API key" } if response.status == 401
  return { error: :auth_failed, message: "API key expired" } if response.status == 403

  begin
    job_data = JSON.parse(response.body)
    return { error: :server_error, message: job_data["error"] } if job_data["error"]

    job_id = job_data["job_id"]
    return { error: :server_error, message: "No job_id returned" } unless job_id

    result = poll_for_completion(job_id, progress: progress, content_path: RUBY_CONTENT_RESPONSES_PATH)
    return { error: :timeout, message: "Audit timed out" } unless result

    { audit_results: result }
  rescue JSON::ParserError => e
    { error: :parse_error, message: "Failed to parse audit response: #{e.message}" }
  end
end

#run_audit_for_ruby_method(other_file, method_info, progress: nil) ⇒ Object



62
63
64
# File 'lib/tng/services/test_generator.rb', line 62

def run_audit_for_ruby_method(other_file, method_info, progress: nil)
  run_audit_for_ruby(other_file, method_info, progress: progress)
end

#run_audit_for_service_method(service, method_info, progress: nil) ⇒ Object



54
55
56
# File 'lib/tng/services/test_generator.rb', line 54

def run_audit_for_service_method(service, method_info, progress: nil)
  run_audit_for_type(service, method_info, :service, progress: progress)
end

#run_audit_for_type(file_object, method_info, type, progress: nil) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/tng/services/test_generator.rb', line 65

def run_audit_for_type(file_object, method_info, type, progress: nil)
  response = send_audit_request_for_type(file_object, method_info, type)
  return { error: :network_error, message: "Network error" } unless response

  if response.is_a?(HTTPX::ErrorResponse)
    return { error: :network_error, message: response.error&.message || "Network error" }
  end

  return { error: :auth_failed, message: "Invalid or missing API key" } if response.status == 401
  return { error: :auth_failed, message: "API key expired" } if response.status == 403

  begin
    job_data = JSON.parse(response.body)
    return { error: :server_error, message: job_data["error"] } if job_data["error"]

    job_id = job_data["job_id"]
    return { error: :server_error, message: "No job_id returned" } unless job_id

    # Poll for completion (audit only)
    result = poll_for_completion(job_id, progress: progress, content_path: CONTENT_RESPONSES_PATH)
    return { error: :timeout, message: "Audit timed out" } unless result

    # Audit results come back as the result directly (not wrapped)
    # Wrap in audit_results key for save_audit_file compatibility
    { audit_results: result }
  rescue JSON::ParserError => e
    { error: :parse_error, message: "Failed to parse audit response: #{e.message}" }
  end
end

#run_for_controller_method(controller, method_info, progress: nil) ⇒ Object



24
25
26
# File 'lib/tng/services/test_generator.rb', line 24

def run_for_controller_method(controller, method_info, progress: nil)
  generate_test_for_type(controller, method_info, :controller, progress: progress)
end

#run_for_model_method(model, method_info, progress: nil) ⇒ Object



28
29
30
# File 'lib/tng/services/test_generator.rb', line 28

def run_for_model_method(model, method_info, progress: nil)
  generate_test_for_type(model, method_info, :model, progress: progress)
end

#run_for_other_method(other_file, method_info, progress: nil) ⇒ Object



36
37
38
# File 'lib/tng/services/test_generator.rb', line 36

def run_for_other_method(other_file, method_info, progress: nil)
  generate_test_for_type(other_file, method_info, :other, progress: progress)
end

#run_for_ruby_method(other_file, method_info, progress: nil) ⇒ Object



40
41
42
# File 'lib/tng/services/test_generator.rb', line 40

def run_for_ruby_method(other_file, method_info, progress: nil)
  generate_test_for_ruby(other_file, method_info, progress: progress)
end

#run_for_service_method(service, method_info, progress: nil) ⇒ Object



32
33
34
# File 'lib/tng/services/test_generator.rb', line 32

def run_for_service_method(service, method_info, progress: nil)
  generate_test_for_type(service, method_info, :service, progress: progress)
end

#send_audit_request_for_type(file_object, method_info, type) ⇒ Object



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/tng/services/test_generator.rb', line 95

def send_audit_request_for_type(file_object, method_info, type)
  config = request_config
  name = file_object[:name] || File.basename(file_object[:path], ".rb")
  enriched_method_info = enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode: false)

  # Pass audit_mode: true as 8th parameter
  case type
  when :controller
    Tng.send_request_for_controller(name, file_object[:path], enriched_method_info, *config, true, false)
  when :model
    Tng.send_request_for_model(name, file_object[:path], enriched_method_info, *config, true, false)
  when :service
    Tng.send_request_for_service(name, file_object[:path], enriched_method_info, *config, true, false)
  when :other
    Tng.send_request_for_other(name, file_object[:path], enriched_method_info, *config, true, false)
  end
end

#send_request_for_ruby(file_object, method_info, audit_mode:) ⇒ Object



224
225
226
227
228
229
230
# File 'lib/tng/services/test_generator.rb', line 224

def send_request_for_ruby(file_object, method_info, audit_mode:)
  config = request_config
  name = file_object[:name] || File.basename(file_object[:path], ".rb")
  enriched_method_info = enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode: true)

  Tng.send_request_for_ruby(name, file_object[:path], enriched_method_info, *config, audit_mode, false)
end

#send_request_for_type(file_object, method_info, type) ⇒ Object



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/tng/services/test_generator.rb', line 149

def send_request_for_type(file_object, method_info, type)
  config = request_config
  name = file_object[:name] || File.basename(file_object[:path], ".rb")
  enriched_method_info = enrich_method_info_with_trace_v2(file_object, method_info, ruby_mode: false)

  # Pass audit_mode: false as 8th parameter for normal test generation
  case type
  when :controller
    Tng.send_request_for_controller(name, file_object[:path], enriched_method_info, *config, false, false)
  when :model
    Tng.send_request_for_model(name, file_object[:path], enriched_method_info, *config, false, false)
  when :service
    Tng.send_request_for_service(name, file_object[:path], enriched_method_info, *config, false, false)
  when :other
    Tng.send_request_for_other(name, file_object[:path], enriched_method_info, *config, false, false)
  end
end

#trigger_cleanup(job_id, content_path: CONTENT_RESPONSES_PATH) ⇒ Object



408
409
410
411
412
# File 'lib/tng/services/test_generator.rb', line 408

def trigger_cleanup(job_id, content_path: CONTENT_RESPONSES_PATH)
  @http_client.patch("#{content_path}/#{job_id}/cleanup")
rescue StandardError => e
  debug_log("Cleanup request failed: #{e.message}") if debug_enabled?
end

#update_progress_from_info(progress, agent_step_indices, info, percent) ⇒ Object



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/tng/services/test_generator.rb', line 361

def update_progress_from_info(progress, agent_step_indices, info, percent)
  return unless progress && info.is_a?(Hash)

  agent_step_indices.each do |key, step_idx|
    item_data = info[key.to_s] || info[key.to_sym]
    next unless item_data

    agent_status = "pending"
    values = []

    if item_data.is_a?(Hash)
      agent_status = item_data[:status] || item_data["status"] || "pending"
      values = item_data[:values] || item_data["values"] || []
    else
      agent_status = item_data.to_s
    end

    label = case key
            when "context_agent_status" then "Context Builder"
            when "style_agent_status" then "Style Analyzer"
            when "logical_issue_status" then "Logic Analyzer"
            when "behavior_expert_status" then "Logic Generator"
            else key
            end

    msg = if agent_status == "processing"
            "#{label}: Processing..."
          elsif agent_status == "completed"
            "#{label}: Completed"
          elsif agent_status == "failed"
            "#{label}: Failed"
          else
            "#{label}: #{agent_status.capitalize}..."
          end

    if values.any?
      clean_vals = values.map { |v| v.to_s.gsub("_", " ").gsub("'", "").gsub(":", "").strip }
      display_str = clean_vals.first(3).join(", ")
      display_str += ", ..." if clean_vals.size > 3
      msg += " (#{display_str})"
    end

    p = (percent && step_idx == agent_step_indices["behavior_expert_status"]) ? percent : nil
    progress.update(msg, p, step_increment: false, explicit_step: step_idx)
  end
end