ruby_raider 3.0.1 → 3.2.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.
Files changed (194) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/e2e_tests.yml +0 -17
  3. data/.github/workflows/system_tests.yml +0 -22
  4. data/README.md +8 -63
  5. data/lib/commands/scaffolding_commands.rb +1 -192
  6. data/lib/commands/utility_commands.rb +0 -52
  7. data/lib/generators/automation/templates/login.tt +1 -9
  8. data/lib/generators/automation/templates/page.tt +1 -7
  9. data/lib/generators/automation/templates/partials/initialize_selector.tt +1 -3
  10. data/lib/generators/automation/templates/partials/visit_method.tt +2 -6
  11. data/lib/generators/common_generator.rb +0 -6
  12. data/lib/generators/cucumber/cucumber_generator.rb +0 -24
  13. data/lib/generators/cucumber/templates/accessibility_steps.tt +2 -6
  14. data/lib/generators/cucumber/templates/cucumber.tt +0 -7
  15. data/lib/generators/cucumber/templates/env.tt +1 -3
  16. data/lib/generators/cucumber/templates/partials/appium_env.tt +1 -6
  17. data/lib/generators/cucumber/templates/partials/selenium_env.tt +0 -13
  18. data/lib/generators/cucumber/templates/partials/watir_env.tt +0 -13
  19. data/lib/generators/cucumber/templates/partials/web_steps.tt +4 -4
  20. data/lib/generators/cucumber/templates/world.tt +1 -3
  21. data/lib/generators/generator.rb +2 -46
  22. data/lib/generators/helper_generator.rb +4 -49
  23. data/lib/generators/infrastructure/templates/github.tt +2 -2
  24. data/lib/generators/infrastructure/templates/github_appium.tt +2 -6
  25. data/lib/generators/invoke_generators.rb +4 -25
  26. data/lib/generators/menu_generator.rb +10 -100
  27. data/lib/generators/rspec/rspec_generator.rb +0 -11
  28. data/lib/generators/rspec/templates/accessibility_spec.tt +2 -6
  29. data/lib/generators/rspec/templates/spec.tt +6 -8
  30. data/lib/generators/templates/common/gemfile.tt +0 -26
  31. data/lib/generators/templates/common/git_ignore.tt +0 -2
  32. data/lib/generators/templates/common/partials/mobile_config.tt +0 -4
  33. data/lib/generators/templates/common/partials/web_config.tt +1 -12
  34. data/lib/generators/templates/common/rakefile.tt +0 -9
  35. data/lib/generators/templates/common/read_me.tt +4 -10
  36. data/lib/generators/templates/helpers/allure_helper.tt +0 -10
  37. data/lib/generators/templates/helpers/browser_helper.tt +0 -5
  38. data/lib/generators/templates/helpers/partials/quit_driver.tt +1 -3
  39. data/lib/generators/templates/helpers/partials/screenshot.tt +1 -3
  40. data/lib/generators/templates/helpers/partials/selenium_driver.tt +0 -5
  41. data/lib/generators/templates/helpers/spec_helper.tt +2 -44
  42. data/lib/generators/templates/helpers/test_helper.tt +0 -7
  43. data/lib/ruby_raider.rb +2 -50
  44. data/lib/scaffolding/project_detector.rb +2 -9
  45. data/lib/scaffolding/scaffolding.rb +0 -109
  46. data/lib/scaffolding/templates/component.tt +1 -4
  47. data/lib/scaffolding/templates/page_object.tt +0 -10
  48. data/lib/scaffolding/templates/spec.tt +2 -6
  49. data/lib/scaffolding/templates/steps.tt +0 -2
  50. data/lib/utilities/utilities.rb +20 -33
  51. data/lib/version +1 -1
  52. data/sig/commands/scaffolding_commands.rbs +0 -12
  53. data/sig/commands/utility_commands.rbs +0 -7
  54. data/sig/generators/cucumber/cucumber_generator.rbs +0 -4
  55. data/sig/generators/generator.rbs +0 -11
  56. data/sig/generators/helper_generator.rbs +1 -6
  57. data/sig/generators/invoke_generators.rbs +1 -2
  58. data/sig/generators/menu_generator.rbs +2 -6
  59. data/sig/generators/rspec/rspec_generator.rbs +0 -2
  60. data/sig/ruby_raider.rbs +1 -3
  61. data/sig/scaffolding/project_detector.rbs +0 -1
  62. data/sig/scaffolding/scaffolding.rbs +0 -23
  63. data/sig/utilities/utilities.rbs +0 -5
  64. data/spec/commands/raider_commands_spec.rb +0 -61
  65. data/spec/integration/commands/browser_update_after_creation_spec.rb +123 -0
  66. data/spec/integration/commands/scaffolding_commands_spec.rb +0 -20
  67. data/spec/integration/commands/utility_commands_spec.rb +5 -5
  68. data/spec/integration/content/ci_content_spec.rb +6 -61
  69. data/spec/integration/content/common_content_spec.rb +0 -64
  70. data/spec/integration/content/config_content_spec.rb +0 -86
  71. data/spec/integration/content/content_helper.rb +2 -2
  72. data/spec/integration/content/gemfile_content_spec.rb +0 -71
  73. data/spec/integration/content/helper_content_spec.rb +2 -284
  74. data/spec/integration/content/page_content_spec.rb +0 -89
  75. data/spec/integration/content/syntax_validation_spec.rb +2 -2
  76. data/spec/integration/content/test_content_spec.rb +0 -119
  77. data/spec/integration/end_to_end_features_spec.rb +13 -435
  78. data/spec/integration/end_to_end_spec.rb +0 -96
  79. data/spec/integration/generators/axe_addon_spec.rb +2 -52
  80. data/spec/integration/generators/common_generator_spec.rb +1 -13
  81. data/spec/integration/generators/config_features_spec.rb +3 -81
  82. data/spec/integration/generators/github_generator_spec.rb +5 -15
  83. data/spec/integration/generators/helpers_generator_spec.rb +0 -37
  84. data/spec/integration/scaffolding_e2e_spec.rb +2 -237
  85. data/spec/integration/settings_helper.rb +1 -3
  86. data/spec/integration/spec_helper.rb +6 -11
  87. data/spec/menus/menu_generator_spec.rb +4 -107
  88. data/spec/scaffolding/scaffold_project_detector_spec.rb +4 -26
  89. data/spec/scaffolding/scaffolding_features_spec.rb +0 -183
  90. data/spec/system/selenium_spec.rb +1 -4
  91. data/spec/system/support/system_test_helper.rb +0 -1
  92. data/spec/system/watir_spec.rb +1 -4
  93. data/spec/utilities/headless_config_spec.rb +0 -7
  94. metadata +3 -102
  95. data/lib/adopter/adopt_menu.rb +0 -146
  96. data/lib/adopter/converters/base_converter.rb +0 -84
  97. data/lib/adopter/converters/identity_converter.rb +0 -53
  98. data/lib/adopter/migration_plan.rb +0 -74
  99. data/lib/adopter/migrator.rb +0 -96
  100. data/lib/adopter/plan_builder.rb +0 -275
  101. data/lib/adopter/project_analyzer.rb +0 -252
  102. data/lib/adopter/project_detector.rb +0 -157
  103. data/lib/generators/cucumber/templates/partials/capybara_env.tt +0 -38
  104. data/lib/generators/cucumber/templates/partials/capybara_world.tt +0 -6
  105. data/lib/generators/cucumber/templates/performance_feature.tt +0 -5
  106. data/lib/generators/cucumber/templates/performance_steps.tt +0 -17
  107. data/lib/generators/cucumber/templates/visual_feature.tt +0 -5
  108. data/lib/generators/cucumber/templates/visual_steps.tt +0 -19
  109. data/lib/generators/infrastructure/gitlab_generator.rb +0 -11
  110. data/lib/generators/infrastructure/templates/gitlab.tt +0 -46
  111. data/lib/generators/minitest/minitest_generator.rb +0 -35
  112. data/lib/generators/minitest/templates/accessibility_test.tt +0 -26
  113. data/lib/generators/minitest/templates/performance_test.tt +0 -18
  114. data/lib/generators/minitest/templates/test.tt +0 -64
  115. data/lib/generators/minitest/templates/visual_test.tt +0 -23
  116. data/lib/generators/rspec/templates/performance_spec.tt +0 -18
  117. data/lib/generators/rspec/templates/visual_spec.tt +0 -20
  118. data/lib/generators/templates/common/ruby_version.tt +0 -1
  119. data/lib/generators/templates/helpers/capybara_helper.tt +0 -32
  120. data/lib/generators/templates/helpers/debug_helper.tt +0 -190
  121. data/lib/generators/templates/helpers/partials/debug_diagnostics.tt +0 -7
  122. data/lib/generators/templates/helpers/partials/debug_start.tt +0 -7
  123. data/lib/generators/templates/helpers/performance_helper.tt +0 -57
  124. data/lib/generators/templates/helpers/visual_helper.tt +0 -58
  125. data/lib/llm/client.rb +0 -79
  126. data/lib/llm/config.rb +0 -57
  127. data/lib/llm/prompts.rb +0 -84
  128. data/lib/llm/provider.rb +0 -27
  129. data/lib/llm/providers/anthropic_provider.rb +0 -43
  130. data/lib/llm/providers/ollama_provider.rb +0 -56
  131. data/lib/llm/providers/openai_provider.rb +0 -42
  132. data/lib/llm/response_parser.rb +0 -67
  133. data/lib/plugin/plugin.rb +0 -111
  134. data/lib/plugin/plugin_exposer.rb +0 -55
  135. data/lib/scaffolding/crud_generator.rb +0 -94
  136. data/lib/scaffolding/dry_run_presenter.rb +0 -16
  137. data/lib/scaffolding/page_introspector.rb +0 -45
  138. data/lib/scaffolding/scaffold_menu.rb +0 -103
  139. data/lib/scaffolding/templates/page_from_url.tt +0 -75
  140. data/lib/scaffolding/templates/spec_from_page.tt +0 -31
  141. data/lib/scaffolding/templates/spec_from_url.tt +0 -46
  142. data/lib/scaffolding/url_analyzer.rb +0 -179
  143. data/sig/adopter/adopt_menu.rbs +0 -25
  144. data/sig/adopter/converters/base_converter.rbs +0 -23
  145. data/sig/adopter/converters/identity_converter.rbs +0 -16
  146. data/sig/adopter/migration_plan.rbs +0 -34
  147. data/sig/adopter/migrator.rbs +0 -21
  148. data/sig/adopter/plan_builder.rbs +0 -38
  149. data/sig/adopter/project_analyzer.rbs +0 -39
  150. data/sig/adopter/project_detector.rbs +0 -26
  151. data/sig/generators/infrastructure/gitlab_generator.rbs +0 -4
  152. data/sig/generators/minitest/minitest_generator.rbs +0 -8
  153. data/sig/llm/client.rbs +0 -15
  154. data/sig/llm/config.rbs +0 -20
  155. data/sig/llm/prompts.rbs +0 -8
  156. data/sig/llm/provider.rbs +0 -12
  157. data/sig/llm/providers/anthropic_provider.rbs +0 -16
  158. data/sig/llm/providers/ollama_provider.rbs +0 -18
  159. data/sig/llm/providers/openai_provider.rbs +0 -16
  160. data/sig/llm/response_parser.rbs +0 -13
  161. data/sig/plugin/plugin.rbs +0 -24
  162. data/sig/plugin/plugin_exposer.rbs +0 -20
  163. data/sig/scaffolding/crud_generator.rbs +0 -16
  164. data/sig/scaffolding/dry_run_presenter.rbs +0 -4
  165. data/sig/scaffolding/page_introspector.rbs +0 -14
  166. data/sig/scaffolding/scaffold_menu.rbs +0 -18
  167. data/sig/scaffolding/url_analyzer.rbs +0 -28
  168. data/spec/adopter/adopt_menu_spec.rb +0 -176
  169. data/spec/adopter/converters/identity_converter_spec.rb +0 -145
  170. data/spec/adopter/migration_plan_spec.rb +0 -113
  171. data/spec/adopter/migrator_spec.rb +0 -277
  172. data/spec/adopter/plan_builder_spec.rb +0 -298
  173. data/spec/adopter/project_analyzer_spec.rb +0 -337
  174. data/spec/adopter/project_detector_spec.rb +0 -295
  175. data/spec/generators/generator_spec.rb +0 -23
  176. data/spec/integration/content/reporter_content_spec.rb +0 -236
  177. data/spec/integration/content/skip_flags_content_spec.rb +0 -206
  178. data/spec/integration/generators/debug_helper_spec.rb +0 -68
  179. data/spec/integration/generators/gitlab_generator_spec.rb +0 -38
  180. data/spec/integration/generators/lighthouse_addon_spec.rb +0 -132
  181. data/spec/integration/generators/minitest_generator_spec.rb +0 -64
  182. data/spec/integration/generators/reporter_spec.rb +0 -159
  183. data/spec/integration/generators/skip_flags_spec.rb +0 -134
  184. data/spec/integration/generators/visual_addon_spec.rb +0 -148
  185. data/spec/llm/client_spec.rb +0 -79
  186. data/spec/llm/config_spec.rb +0 -92
  187. data/spec/llm/prompts_spec.rb +0 -49
  188. data/spec/llm/response_parser_spec.rb +0 -92
  189. data/spec/menus/adopter_adopt_menu_spec.rb +0 -97
  190. data/spec/scaffolding/page_introspector_spec.rb +0 -82
  191. data/spec/scaffolding/url_analyzer_spec.rb +0 -110
  192. data/spec/system/adopt_matrix_spec.rb +0 -537
  193. data/spec/system/adopt_spec.rb +0 -225
  194. data/spec/system/capybara_spec.rb +0 -42
@@ -1,190 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'fileutils'
4
- require 'yaml'
5
- require 'logger'
6
- require 'json'
7
-
8
- module DebugHelper
9
- LOG_DIR = 'debug_logs'
10
-
11
- module_function
12
-
13
- # --- Configuration ---
14
-
15
- def config
16
- @config ||= begin
17
- yml = YAML.load_file('config/config.yml')
18
- yml['debug'] || {}
19
- rescue StandardError
20
- {}
21
- end
22
- end
23
-
24
- def enabled?
25
- ENV['DEBUG']&.downcase == 'true' || config.fetch('enabled', false)
26
- end
27
-
28
- def console_logs?
29
- enabled? && config.fetch('console_logs', true)
30
- end
31
-
32
- def action_logging?
33
- enabled? && config.fetch('action_logging', true)
34
- end
35
-
36
- def network_logging?
37
- enabled? && config.fetch('network_logging', true)
38
- end
39
-
40
- def log_dir
41
- config.fetch('log_dir', LOG_DIR)
42
- end
43
-
44
- def ensure_log_dir
45
- FileUtils.mkdir_p(log_dir)
46
- end
47
-
48
- # --- Driver Resolution ---
49
-
50
- def resolve_selenium_driver(obj)
51
- if obj.respond_to?(:driver) && !obj.is_a?(Selenium::WebDriver::Driver)
52
- obj.driver
53
- elsif obj.respond_to?(:browser) && obj.browser.respond_to?(:driver)
54
- obj.browser.driver
55
- else
56
- obj
57
- end
58
- rescue StandardError
59
- obj
60
- end
61
-
62
- # --- Failure Diagnostics (returns structured hash for desktop integration) ---
63
-
64
- def capture_failure_diagnostics(driver_or_browser, test_name, exception: nil)
65
- return {} unless enabled?
66
-
67
- ensure_log_dir
68
- sel_driver = resolve_selenium_driver(driver_or_browser)
69
- safe_name = sanitize(test_name)
70
- diagnostics = { test_name: test_name, timestamp: Time.now.iso8601 }
71
-
72
- diagnostics[:url] = sel_driver.current_url rescue 'unknown'
73
- diagnostics[:title] = sel_driver.title rescue 'unknown'
74
-
75
- if exception
76
- diagnostics[:error_message] = exception.message
77
- diagnostics[:error_class] = exception.class.name
78
- diagnostics[:stack_trace] = exception.backtrace&.first(20) || []
79
- end
80
-
81
- diagnostics[:console_logs] = capture_console_logs(sel_driver) if console_logs?
82
- diagnostics[:network_log_path] = capture_network_logs(sel_driver, safe_name) if network_logging?
83
-
84
- # HTML snapshot
85
- html_path = File.join(log_dir, "#{safe_name}_page.html")
86
- File.write(html_path, sel_driver.page_source) rescue nil
87
- diagnostics[:html_snapshot] = html_path
88
-
89
- # Write diagnostics JSON (desktop app reads this)
90
- summary_path = File.join(log_dir, "#{safe_name}_diagnostics.json")
91
- File.write(summary_path, JSON.pretty_generate(diagnostics))
92
- diagnostics[:summary_path] = summary_path
93
-
94
- diagnostics
95
- rescue StandardError => e
96
- warn "[debug] Failed to capture diagnostics: #{e.message}"
97
- {}
98
- end
99
-
100
- # --- Console Log Capture ---
101
-
102
- def capture_console_logs(sel_driver)
103
- return [] unless chrome_or_edge?(sel_driver)
104
-
105
- sel_driver.logs.get(:browser).map { |entry| { level: entry.level, message: entry.message, timestamp: entry.timestamp } }
106
- rescue StandardError
107
- []
108
- end
109
-
110
- # --- Action Logger ---
111
-
112
- class ActionLogger
113
- def initialize(test_name)
114
- DebugHelper.ensure_log_dir
115
- log_path = File.join(DebugHelper.log_dir, "#{DebugHelper.sanitize(test_name)}_actions.log")
116
- @logger = Logger.new(log_path)
117
- @logger.formatter = proc { |severity, datetime, _progname, msg|
118
- "[#{datetime.strftime('%H:%M:%S.%L')}] #{severity}: #{msg}\n"
119
- }
120
- end
121
-
122
- def log(action, details = '')
123
- @logger.info("#{action} #{details}".strip)
124
- end
125
-
126
- def close
127
- @logger.close
128
- rescue StandardError
129
- nil
130
- end
131
- end
132
-
133
- class NullActionLogger
134
- def log(_action, _details = '') = nil
135
- def close = nil
136
- end
137
-
138
- def action_logger_for(test_name)
139
- action_logging? ? ActionLogger.new(test_name) : NullActionLogger.new
140
- end
141
-
142
- # --- Network Logging Setup ---
143
-
144
- def enable_network_logging(driver_or_browser)
145
- return unless network_logging?
146
-
147
- sel_driver = resolve_selenium_driver(driver_or_browser)
148
- return unless chrome_or_edge?(sel_driver)
149
-
150
- sel_driver.execute_cdp('Network.enable')
151
- rescue StandardError => e
152
- warn "[debug] Could not enable network logging: #{e.message}"
153
- end
154
-
155
- # --- Network Log Capture ---
156
-
157
- def capture_network_logs(sel_driver, safe_name)
158
- return unless chrome_or_edge?(sel_driver)
159
-
160
- logs = sel_driver.logs.get(:performance)
161
- return if logs.empty?
162
-
163
- entries = logs.filter_map do |entry|
164
- parsed = JSON.parse(entry.message)['message'] rescue next
165
- parsed if parsed.is_a?(Hash) && parsed['method']&.start_with?('Network.')
166
- end
167
-
168
- return if entries.empty?
169
-
170
- network_path = File.join(log_dir, "#{safe_name}_network.json")
171
- File.write(network_path, JSON.pretty_generate(entries))
172
- network_path
173
- rescue StandardError => e
174
- warn "[debug] Failed to capture network logs: #{e.message}"
175
- nil
176
- end
177
-
178
- # --- Helpers ---
179
-
180
- def chrome_or_edge?(driver)
181
- browser_name = driver.capabilities[:browser_name].to_s.downcase
182
- %w[chrome chromium msedge edge].any? { |b| browser_name.include?(b) }
183
- rescue StandardError
184
- false
185
- end
186
-
187
- def sanitize(name)
188
- name.to_s.gsub(/[^a-zA-Z0-9_-]/, '_')[0..80]
189
- end
190
- end
@@ -1,7 +0,0 @@
1
- <% if capybara? %>
2
- DebugHelper.capture_failure_diagnostics(Capybara.current_session.driver, example_name, exception: @_exception)
3
- <% elsif selenium_based? %>
4
- DebugHelper.capture_failure_diagnostics(driver, example_name, exception: @_exception)
5
- <% elsif watir? %>
6
- DebugHelper.capture_failure_diagnostics(browser, example_name, exception: @_exception)
7
- <% end %>
@@ -1,7 +0,0 @@
1
- <% if capybara? %>
2
- DebugHelper.enable_network_logging(Capybara.current_session.driver)
3
- <% elsif selenium_based? %>
4
- DebugHelper.enable_network_logging(driver)
5
- <% elsif watir? %>
6
- DebugHelper.enable_network_logging(browser)
7
- <% end %>
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'open3'
4
- require 'json'
5
- require 'fileutils'
6
- require 'yaml'
7
-
8
- module PerformanceHelper
9
- REPORTS_DIR = 'lighthouse_reports'
10
-
11
- def run_lighthouse_audit(url, categories: %w[performance accessibility best-practices seo])
12
- FileUtils.mkdir_p(REPORTS_DIR)
13
-
14
- report_path = File.join(REPORTS_DIR, "report_#{Time.now.to_i}.json")
15
- category_flags = categories.map { |c| "--only-categories=#{c}" }.join(' ')
16
-
17
- config_timeout = if File.exist?('config/config.yml')
18
- YAML.load_file('config/config.yml').fetch('timeout', 10)
19
- else
20
- 10
21
- end
22
- max_wait = config_timeout * 1000
23
-
24
- command = "lighthouse #{url} --output=json --output-path=#{report_path} " \
25
- "--chrome-flags=\"--headless --no-sandbox\" #{category_flags} " \
26
- "--max-wait-for-load=#{max_wait} --quiet"
27
-
28
- stdout, stderr, status = Open3.capture3(command)
29
-
30
- unless status.success?
31
- return { status: :error, message: "Lighthouse failed: #{stderr.strip.empty? ? stdout : stderr}" }
32
- end
33
-
34
- report = JSON.parse(File.read(report_path))
35
- scores = extract_scores(report)
36
-
37
- { status: :success, scores:, report_path: }
38
- end
39
-
40
- def assert_performance_above(url, threshold: 0.8, categories: %w[performance])
41
- result = run_lighthouse_audit(url, categories:)
42
- return result if result[:status] == :error
43
-
44
- passed = result[:scores].all? { |_category, score| score >= threshold }
45
-
46
- { status: passed ? :pass : :fail, scores: result[:scores], passed:, threshold:,
47
- report_path: result[:report_path] }
48
- end
49
-
50
- private
51
-
52
- def extract_scores(report)
53
- report.fetch('categories', {}).each_with_object({}) do |(key, data), scores|
54
- scores[key] = data['score']
55
- end
56
- end
57
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'chunky_png'
4
- require 'fileutils'
5
-
6
- module VisualHelper
7
- BASELINE_DIR = 'visual_baselines'
8
- DIFF_DIR = 'visual_diffs'
9
-
10
- def compare_screenshot(name, screenshot_path, threshold: 0.01)
11
- FileUtils.mkdir_p(BASELINE_DIR)
12
- FileUtils.mkdir_p(DIFF_DIR)
13
-
14
- baseline_path = File.join(BASELINE_DIR, "#{name}.png")
15
-
16
- unless File.exist?(baseline_path)
17
- FileUtils.cp(screenshot_path, baseline_path)
18
- return { status: :baseline_created, path: baseline_path }
19
- end
20
-
21
- baseline = ChunkyPNG::Image.from_file(baseline_path)
22
- current = ChunkyPNG::Image.from_file(screenshot_path)
23
-
24
- diff_pixels = 0
25
- total_pixels = baseline.width * baseline.height
26
-
27
- diff_image = ChunkyPNG::Image.new(baseline.width, baseline.height)
28
-
29
- baseline.height.times do |y|
30
- baseline.width.times do |x|
31
- if pixel_match?(baseline[x, y], current[x, y])
32
- diff_image[x, y] = ChunkyPNG::Color.rgba(0, 0, 0, 50)
33
- else
34
- diff_pixels += 1
35
- diff_image[x, y] = ChunkyPNG::Color.rgb(255, 0, 0)
36
- end
37
- end
38
- end
39
-
40
- diff_percentage = diff_pixels.to_f / total_pixels
41
-
42
- if diff_percentage > threshold
43
- diff_path = File.join(DIFF_DIR, "#{name}_diff.png")
44
- diff_image.save(diff_path)
45
- { status: :mismatch, diff: diff_percentage, diff_path: }
46
- else
47
- { status: :match, diff: diff_percentage }
48
- end
49
- end
50
-
51
- private
52
-
53
- def pixel_match?(pixel1, pixel2)
54
- r1, g1, b1 = ChunkyPNG::Color.r(pixel1), ChunkyPNG::Color.g(pixel1), ChunkyPNG::Color.b(pixel1)
55
- r2, g2, b2 = ChunkyPNG::Color.r(pixel2), ChunkyPNG::Color.g(pixel2), ChunkyPNG::Color.b(pixel2)
56
- (r1 - r2).abs <= 5 && (g1 - g2).abs <= 5 && (b1 - b2).abs <= 5
57
- end
58
- end
data/lib/llm/client.rb DELETED
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'net/http'
4
- require_relative 'config'
5
-
6
- module Llm
7
- # Facade for LLM completion with retry logic and graceful fallback.
8
- # Returns nil on any failure — callers should always have a non-AI fallback.
9
- module Client
10
- MAX_RETRIES = 3
11
- BASE_DELAY = 1
12
-
13
- # Only retry on transient network errors, not configuration or API errors
14
- RETRYABLE_ERRORS = [
15
- Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED,
16
- Errno::ECONNRESET, Errno::ETIMEDOUT, SocketError
17
- ].freeze
18
-
19
- class << self
20
- def complete(prompt, system_prompt: nil)
21
- provider = build_provider
22
- return nil unless provider
23
-
24
- attempt = 0
25
- begin
26
- attempt += 1
27
- provider.complete(prompt, system_prompt:)
28
- rescue *RETRYABLE_ERRORS => e
29
- if attempt < MAX_RETRIES
30
- sleep(BASE_DELAY * (2**(attempt - 1)))
31
- retry
32
- end
33
- warn "[Ruby Raider] LLM failed after #{MAX_RETRIES} attempts: #{e.message}"
34
- nil
35
- rescue StandardError => e
36
- warn "[Ruby Raider] LLM error: #{e.message}"
37
- nil
38
- end
39
- end
40
-
41
- def available?
42
- config = Config.new
43
- return false unless config.configured?
44
-
45
- provider = config.build_provider
46
- provider&.available? || false
47
- end
48
-
49
- def status
50
- config = Config.new
51
- return { configured: false, provider: nil } unless config.configured?
52
-
53
- provider = config.build_provider
54
- {
55
- configured: true,
56
- provider: config.provider_name,
57
- model: config.model,
58
- available: provider&.available? || false
59
- }
60
- end
61
-
62
- private
63
-
64
- def build_provider
65
- config = Config.new
66
- unless config.configured?
67
- warn '[Ruby Raider] No LLM configured. Use: raider u llm ollama'
68
- return nil
69
- end
70
- provider = config.build_provider
71
- unless provider&.available?
72
- warn "[Ruby Raider] LLM provider '#{config.provider_name}' is not available"
73
- return nil
74
- end
75
- provider
76
- end
77
- end
78
- end
79
- end
data/lib/llm/config.rb DELETED
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'yaml'
4
-
5
- module Llm
6
- # Reads LLM configuration from env vars and config/config.yml
7
- # Env vars take precedence over config file values.
8
- class Config
9
- PROVIDERS = %w[openai anthropic ollama].freeze
10
-
11
- attr_reader :provider_name, :api_key, :model, :url
12
-
13
- def initialize
14
- @provider_name = env('RUBY_RAIDER_LLM_PROVIDER') || config_value('llm_provider')
15
- @api_key = env('RUBY_RAIDER_LLM_API_KEY') || config_value('llm_api_key')
16
- @model = env('RUBY_RAIDER_LLM_MODEL') || config_value('llm_model')
17
- @url = env('RUBY_RAIDER_LLM_URL') || config_value('llm_url')
18
- end
19
-
20
- def configured?
21
- return false unless @provider_name && PROVIDERS.include?(@provider_name)
22
- return true if @provider_name == 'ollama'
23
-
24
- !@api_key.nil? && !@api_key.empty?
25
- end
26
-
27
- def build_provider
28
- return nil unless configured?
29
-
30
- case @provider_name
31
- when 'openai'
32
- require_relative 'providers/openai_provider'
33
- Providers::OpenaiProvider.new(api_key: @api_key, model: @model)
34
- when 'anthropic'
35
- require_relative 'providers/anthropic_provider'
36
- Providers::AnthropicProvider.new(api_key: @api_key, model: @model)
37
- when 'ollama'
38
- require_relative 'providers/ollama_provider'
39
- Providers::OllamaProvider.new(model: @model, url: @url)
40
- end
41
- end
42
-
43
- private
44
-
45
- def env(key)
46
- value = ENV.fetch(key, nil)
47
- value&.strip&.empty? ? nil : value&.strip
48
- end
49
-
50
- def config_value(key)
51
- return nil unless File.exist?('config/config.yml')
52
-
53
- data = YAML.load_file('config/config.yml')
54
- data&.dig(key)
55
- end
56
- end
57
- end
data/lib/llm/prompts.rb DELETED
@@ -1,84 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Llm
4
- # Centralized prompt templates for LLM-enhanced code generation.
5
- # All prompts instruct the model to return JSON matching specific schemas.
6
- module Prompts
7
- module_function
8
-
9
- def analyze_page(html, url)
10
- <<~PROMPT
11
- Analyze this HTML page and extract all interactive elements that would be useful for a page object model in UI test automation.
12
-
13
- URL: #{url}
14
-
15
- HTML:
16
- #{html[0..8000]}
17
-
18
- Return a JSON object with this exact structure:
19
- {
20
- "elements": [
21
- {
22
- "name": "descriptive_snake_case_name",
23
- "type": "input|select|textarea|button|submit|link",
24
- "locator": {"type": "id|name|css|xpath", "value": "the_locator"},
25
- "purpose": "brief description of what this element does",
26
- "input_type": "text|email|password|etc (only for inputs)",
27
- "text": "visible text (only for buttons/links)"
28
- }
29
- ]
30
- }
31
-
32
- Guidelines:
33
- - Use semantic, descriptive names (e.g., "email_field" not "input_1", "submit_login" not "button_1")
34
- - Prefer ID locators, then name, then CSS, then XPath
35
- - Skip hidden inputs and purely decorative elements
36
- - Include up to 5 important links
37
- - The "purpose" field should be a brief, clear description
38
-
39
- Return ONLY the JSON object, no other text.
40
- PROMPT
41
- end
42
-
43
- def generate_test_scenarios(class_name, methods, automation, framework)
44
- methods_desc = methods.map do |m|
45
- params = m[:params].empty? ? '' : "(#{m[:params].join(', ')})"
46
- " - #{m[:name]}#{params}"
47
- end.join("\n")
48
-
49
- <<~PROMPT
50
- Generate meaningful test scenarios for this page object class used in UI test automation.
51
-
52
- Class: #{class_name}
53
- Automation: #{automation}
54
- Framework: #{framework}
55
- Public methods:
56
- #{methods_desc}
57
-
58
- Return a JSON object with this exact structure:
59
- {
60
- "scenarios": [
61
- {
62
- "method": "method_name",
63
- "description": "human-readable test description",
64
- "assertion_hint": "what to assert after calling this method"
65
- }
66
- ]
67
- }
68
-
69
- Guidelines:
70
- - Generate one scenario per method
71
- - Descriptions should read naturally (e.g., "fills in the login form with valid credentials")
72
- - Assertion hints should be specific (e.g., "expect page to redirect to dashboard")
73
- - Consider the class name for context about what page this is
74
-
75
- Return ONLY the JSON object, no other text.
76
- PROMPT
77
- end
78
-
79
- def system_prompt
80
- 'You are a UI test automation expert. You generate clean, idiomatic Ruby code ' \
81
- 'for page object models and test specs. Always return valid JSON when asked for JSON.'
82
- end
83
- end
84
- end
data/lib/llm/provider.rb DELETED
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Llm
4
- # Abstract base class for LLM providers
5
- class Provider
6
- def complete(prompt, system_prompt: nil)
7
- raise NotImplementedError, "#{self.class}#complete must be implemented"
8
- end
9
-
10
- def available?
11
- raise NotImplementedError, "#{self.class}#available? must be implemented"
12
- end
13
-
14
- def name
15
- self.class.name.split('::').last.sub('Provider', '').downcase
16
- end
17
-
18
- private
19
-
20
- def build_messages(prompt, system_prompt)
21
- messages = []
22
- messages << { role: 'system', content: system_prompt } if system_prompt
23
- messages << { role: 'user', content: prompt }
24
- messages
25
- end
26
- end
27
- end
@@ -1,43 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative '../provider'
4
-
5
- module Llm
6
- module Providers
7
- class AnthropicProvider < Provider
8
- DEFAULT_MODEL = 'claude-sonnet-4-20250514'
9
-
10
- def initialize(api_key:, model: nil)
11
- super()
12
- @api_key = api_key
13
- @model = model || DEFAULT_MODEL
14
- end
15
-
16
- def complete(prompt, system_prompt: nil)
17
- client = build_client
18
- params = { model: @model, max_tokens: 4096, messages: [{ role: 'user', content: prompt }] }
19
- params[:system] = system_prompt if system_prompt
20
- response = client.messages(parameters: params)
21
- response.dig('content', 0, 'text')
22
- rescue StandardError => e
23
- warn "[Ruby Raider] Anthropic error: #{e.message}"
24
- nil
25
- end
26
-
27
- def available?
28
- require 'anthropic'
29
- !@api_key.nil? && !@api_key.empty?
30
- rescue LoadError
31
- warn '[Ruby Raider] Install anthropic gem: gem install anthropic-rb'
32
- false
33
- end
34
-
35
- private
36
-
37
- def build_client
38
- require 'anthropic'
39
- Anthropic::Client.new(access_token: @api_key)
40
- end
41
- end
42
- end
43
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'net/http'
4
- require 'json'
5
- require 'uri'
6
- require_relative '../provider'
7
-
8
- module Llm
9
- module Providers
10
- class OllamaProvider < Provider
11
- DEFAULT_MODEL = 'llama3.2'
12
- DEFAULT_URL = 'http://localhost:11434'
13
- TIMEOUT = 60
14
-
15
- def initialize(model: nil, url: nil)
16
- super()
17
- @model = model || DEFAULT_MODEL
18
- @base_url = url || DEFAULT_URL
19
- end
20
-
21
- def complete(prompt, system_prompt: nil)
22
- uri = URI("#{@base_url}/api/generate")
23
- body = { model: @model, prompt:, stream: false }
24
- body[:system] = system_prompt if system_prompt
25
-
26
- response = post_request(uri, body)
27
- return nil unless response.is_a?(Net::HTTPSuccess)
28
-
29
- JSON.parse(response.body)['response']
30
- rescue StandardError => e
31
- warn "[Ruby Raider] Ollama error: #{e.message}"
32
- nil
33
- end
34
-
35
- def available?
36
- uri = URI("#{@base_url}/api/tags")
37
- response = Net::HTTP.get_response(uri)
38
- response.is_a?(Net::HTTPSuccess)
39
- rescue StandardError
40
- warn '[Ruby Raider] Ollama not reachable. Start it with: ollama serve'
41
- false
42
- end
43
-
44
- private
45
-
46
- def post_request(uri, body)
47
- http = Net::HTTP.new(uri.host, uri.port)
48
- http.read_timeout = TIMEOUT
49
- http.open_timeout = 10
50
- request = Net::HTTP::Post.new(uri, 'Content-Type' => 'application/json')
51
- request.body = JSON.generate(body)
52
- http.request(request)
53
- end
54
- end
55
- end
56
- end