ruby_raider 2.0.0 → 3.0.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 (228) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/e2e_tests.yml +58 -0
  3. data/.github/workflows/steep.yml +21 -0
  4. data/.gitignore +1 -1
  5. data/.reek.yml +46 -4
  6. data/.ruby-version +1 -1
  7. data/README.md +138 -77
  8. data/Steepfile +22 -0
  9. data/assets/ruby_raider_logo.svg +51 -0
  10. data/lib/adopter/adopt_menu.rb +11 -15
  11. data/lib/adopter/converters/base_converter.rb +1 -2
  12. data/lib/adopter/converters/identity_converter.rb +3 -6
  13. data/lib/adopter/migration_plan.rb +0 -1
  14. data/lib/adopter/plan_builder.rb +2 -5
  15. data/lib/adopter/project_analyzer.rb +1 -5
  16. data/lib/adopter/project_detector.rb +3 -5
  17. data/lib/commands/adopt_commands.rb +0 -1
  18. data/lib/commands/plugin_commands.rb +0 -2
  19. data/lib/commands/scaffolding_commands.rb +246 -38
  20. data/lib/commands/utility_commands.rb +92 -3
  21. data/lib/generators/automation/automation_generator.rb +0 -7
  22. data/lib/generators/automation/templates/partials/element.tt +1 -1
  23. data/lib/generators/automation/templates/partials/initialize_selector.tt +0 -7
  24. data/lib/generators/automation/templates/partials/url_methods.tt +0 -1
  25. data/lib/generators/common_generator.rb +12 -0
  26. data/lib/generators/cucumber/cucumber_generator.rb +36 -0
  27. data/lib/generators/cucumber/templates/accessibility_feature.tt +5 -0
  28. data/lib/generators/cucumber/templates/accessibility_steps.tt +21 -0
  29. data/lib/generators/cucumber/templates/cucumber.tt +8 -1
  30. data/lib/generators/cucumber/templates/feature.tt +0 -4
  31. data/lib/generators/cucumber/templates/partials/appium_env.tt +5 -0
  32. data/lib/generators/cucumber/templates/partials/capybara_env.tt +19 -1
  33. data/lib/generators/cucumber/templates/partials/driver_world.tt +1 -4
  34. data/lib/generators/cucumber/templates/partials/selenium_env.tt +22 -35
  35. data/lib/generators/cucumber/templates/partials/watir_env.tt +20 -1
  36. data/lib/generators/cucumber/templates/partials/web_steps.tt +6 -12
  37. data/lib/generators/cucumber/templates/performance_feature.tt +5 -0
  38. data/lib/generators/cucumber/templates/performance_steps.tt +17 -0
  39. data/lib/generators/cucumber/templates/visual_feature.tt +5 -0
  40. data/lib/generators/cucumber/templates/visual_steps.tt +19 -0
  41. data/lib/generators/generator.rb +38 -7
  42. data/lib/generators/helper_generator.rb +24 -7
  43. data/lib/generators/infrastructure/templates/github.tt +1 -1
  44. data/lib/generators/infrastructure/templates/github_appium.tt +2 -2
  45. data/lib/generators/infrastructure/templates/gitlab.tt +1 -1
  46. data/lib/generators/invoke_generators.rb +42 -9
  47. data/lib/generators/menu_generator.rb +120 -11
  48. data/lib/generators/minitest/minitest_generator.rb +16 -4
  49. data/lib/generators/minitest/templates/accessibility_test.tt +26 -0
  50. data/lib/generators/minitest/templates/performance_test.tt +18 -0
  51. data/lib/generators/minitest/templates/test.tt +5 -34
  52. data/lib/generators/minitest/templates/visual_test.tt +23 -0
  53. data/lib/generators/rspec/rspec_generator.rb +16 -4
  54. data/lib/generators/rspec/templates/accessibility_spec.tt +25 -0
  55. data/lib/generators/rspec/templates/performance_spec.tt +18 -0
  56. data/lib/generators/rspec/templates/spec.tt +5 -35
  57. data/lib/generators/rspec/templates/visual_spec.tt +20 -0
  58. data/lib/generators/template_renderer/partial_cache.rb +11 -1
  59. data/lib/generators/template_renderer/partial_resolver.rb +17 -10
  60. data/lib/generators/template_renderer.rb +17 -1
  61. data/lib/generators/templates/common/gemfile.tt +21 -6
  62. data/lib/generators/templates/common/git_ignore.tt +6 -1
  63. data/lib/generators/templates/common/partials/mobile_config.tt +5 -1
  64. data/lib/generators/templates/common/partials/web_config.tt +16 -7
  65. data/lib/generators/templates/common/rakefile.tt +36 -0
  66. data/lib/generators/templates/common/read_me.tt +41 -91
  67. data/lib/generators/templates/common/rspec.tt +3 -0
  68. data/lib/generators/templates/common/ruby_version.tt +1 -0
  69. data/lib/generators/templates/helpers/allure_helper.tt +11 -0
  70. data/lib/generators/templates/helpers/browser_helper.tt +13 -3
  71. data/lib/generators/templates/helpers/capybara_helper.tt +6 -2
  72. data/lib/generators/templates/helpers/debug_helper.tt +190 -0
  73. data/lib/generators/templates/helpers/driver_helper.tt +2 -10
  74. data/lib/generators/templates/helpers/partials/appium_driver.tt +0 -2
  75. data/lib/generators/templates/helpers/partials/debug_diagnostics.tt +7 -0
  76. data/lib/generators/templates/helpers/partials/debug_start.tt +7 -0
  77. data/lib/generators/templates/helpers/partials/driver_and_options.tt +1 -3
  78. data/lib/generators/templates/helpers/partials/selenium_driver.tt +9 -8
  79. data/lib/generators/templates/helpers/partials/video_start.tt +9 -0
  80. data/lib/generators/templates/helpers/partials/video_stop.tt +4 -0
  81. data/lib/generators/templates/helpers/performance_helper.tt +57 -0
  82. data/lib/generators/templates/helpers/spec_helper.tt +57 -8
  83. data/lib/generators/templates/helpers/test_helper.tt +69 -1
  84. data/lib/generators/templates/helpers/video_helper.tt +270 -0
  85. data/lib/generators/templates/helpers/visual_helper.tt +39 -46
  86. data/lib/llm/client.rb +79 -0
  87. data/lib/llm/config.rb +57 -0
  88. data/lib/llm/prompts.rb +84 -0
  89. data/lib/llm/provider.rb +27 -0
  90. data/lib/llm/providers/anthropic_provider.rb +43 -0
  91. data/lib/llm/providers/ollama_provider.rb +56 -0
  92. data/lib/llm/providers/openai_provider.rb +42 -0
  93. data/lib/llm/response_parser.rb +67 -0
  94. data/lib/plugin/plugin.rb +22 -20
  95. data/lib/plugin/plugin_exposer.rb +16 -38
  96. data/lib/ruby_raider.rb +47 -12
  97. data/lib/scaffolding/crud_generator.rb +94 -0
  98. data/lib/scaffolding/dry_run_presenter.rb +16 -0
  99. data/lib/scaffolding/name_normalizer.rb +63 -0
  100. data/lib/scaffolding/page_introspector.rb +45 -0
  101. data/lib/scaffolding/project_detector.rb +96 -0
  102. data/lib/scaffolding/scaffold_menu.rb +103 -0
  103. data/lib/scaffolding/scaffolding.rb +158 -11
  104. data/lib/scaffolding/templates/component.tt +30 -0
  105. data/lib/scaffolding/templates/feature.tt +4 -4
  106. data/lib/scaffolding/templates/helper.tt +15 -1
  107. data/lib/scaffolding/templates/page_from_url.tt +75 -0
  108. data/lib/scaffolding/templates/page_object.tt +50 -1
  109. data/lib/scaffolding/templates/spec.tt +33 -2
  110. data/lib/scaffolding/templates/spec_from_page.tt +31 -0
  111. data/lib/scaffolding/templates/spec_from_url.tt +46 -0
  112. data/lib/scaffolding/templates/steps.tt +17 -5
  113. data/lib/scaffolding/url_analyzer.rb +179 -0
  114. data/lib/utilities/desktop_downloader.rb +177 -0
  115. data/lib/utilities/logo.rb +83 -0
  116. data/lib/utilities/utilities.rb +61 -20
  117. data/lib/version +1 -1
  118. data/ruby_raider.gemspec +1 -0
  119. data/sig/adopter/adopt_menu.rbs +25 -0
  120. data/sig/adopter/converters/base_converter.rbs +23 -0
  121. data/sig/adopter/converters/identity_converter.rbs +16 -0
  122. data/sig/adopter/migration_plan.rbs +34 -0
  123. data/sig/adopter/migrator.rbs +21 -0
  124. data/sig/adopter/plan_builder.rbs +38 -0
  125. data/sig/adopter/project_analyzer.rbs +39 -0
  126. data/sig/adopter/project_detector.rbs +26 -0
  127. data/sig/commands/adopt_commands.rbs +8 -0
  128. data/sig/commands/loaded_commands.rbs +5 -0
  129. data/sig/commands/plugin_commands.rbs +9 -0
  130. data/sig/commands/scaffolding_commands.rbs +28 -0
  131. data/sig/commands/utility_commands.rbs +21 -0
  132. data/sig/generators/automation/automation_generator.rbs +20 -0
  133. data/sig/generators/common_generator.rbs +12 -0
  134. data/sig/generators/cucumber/cucumber_generator.rbs +16 -0
  135. data/sig/generators/generator.rbs +40 -0
  136. data/sig/generators/helper_generator.rbs +18 -0
  137. data/sig/generators/infrastructure/github_generator.rbs +5 -0
  138. data/sig/generators/infrastructure/gitlab_generator.rbs +4 -0
  139. data/sig/generators/invoke_generators.rbs +10 -0
  140. data/sig/generators/menu_generator.rbs +29 -0
  141. data/sig/generators/minitest/minitest_generator.rbs +8 -0
  142. data/sig/generators/rspec/rspec_generator.rbs +8 -0
  143. data/sig/generators/template_renderer/partial_cache.rbs +20 -0
  144. data/sig/generators/template_renderer/partial_resolver.rbs +20 -0
  145. data/sig/generators/template_renderer/template_error.rbs +19 -0
  146. data/sig/generators/template_renderer.rbs +10 -0
  147. data/sig/llm/client.rbs +15 -0
  148. data/sig/llm/config.rbs +20 -0
  149. data/sig/llm/prompts.rbs +8 -0
  150. data/sig/llm/provider.rbs +12 -0
  151. data/sig/llm/providers/anthropic_provider.rbs +16 -0
  152. data/sig/llm/providers/ollama_provider.rbs +18 -0
  153. data/sig/llm/providers/openai_provider.rbs +16 -0
  154. data/sig/llm/response_parser.rbs +13 -0
  155. data/sig/plugin/plugin.rbs +24 -0
  156. data/sig/plugin/plugin_exposer.rbs +20 -0
  157. data/sig/ruby_raider.rbs +15 -0
  158. data/sig/scaffolding/crud_generator.rbs +16 -0
  159. data/sig/scaffolding/dry_run_presenter.rbs +4 -0
  160. data/sig/scaffolding/name_normalizer.rbs +17 -0
  161. data/sig/scaffolding/page_introspector.rbs +14 -0
  162. data/sig/scaffolding/project_detector.rbs +14 -0
  163. data/sig/scaffolding/scaffold_menu.rbs +18 -0
  164. data/sig/scaffolding/scaffolding.rbs +55 -0
  165. data/sig/scaffolding/url_analyzer.rbs +28 -0
  166. data/sig/utilities/desktop_downloader.rbs +23 -0
  167. data/sig/utilities/logger.rbs +13 -0
  168. data/sig/utilities/logo.rbs +16 -0
  169. data/sig/utilities/utilities.rbs +30 -0
  170. data/sig/vendor/thor.rbs +34 -0
  171. data/sig/vendor/tty_prompt.rbs +15 -0
  172. data/spec/adopter/adopt_menu_spec.rb +12 -12
  173. data/spec/adopter/migration_plan_spec.rb +1 -1
  174. data/spec/adopter/migrator_spec.rb +2 -2
  175. data/spec/adopter/project_detector_spec.rb +1 -1
  176. data/spec/commands/raider_commands_spec.rb +129 -0
  177. data/spec/commands/scaffolding_commands_spec.rb +22 -0
  178. data/spec/generators/generator_spec.rb +23 -0
  179. data/spec/integration/commands/scaffolding_commands_spec.rb +1 -1
  180. data/spec/integration/commands/utility_commands_spec.rb +29 -9
  181. data/spec/integration/content/ci_content_spec.rb +119 -0
  182. data/spec/integration/content/common_content_spec.rb +288 -0
  183. data/spec/integration/content/config_content_spec.rb +175 -0
  184. data/spec/integration/content/content_helper.rb +32 -0
  185. data/spec/integration/content/gemfile_content_spec.rb +209 -0
  186. data/spec/integration/content/helper_content_spec.rb +485 -0
  187. data/spec/integration/content/page_content_spec.rb +259 -0
  188. data/spec/integration/content/reporter_content_spec.rb +236 -0
  189. data/spec/integration/content/skip_flags_content_spec.rb +206 -0
  190. data/spec/integration/content/syntax_validation_spec.rb +30 -0
  191. data/spec/integration/content/test_content_spec.rb +266 -0
  192. data/spec/integration/end_to_end_features_spec.rb +690 -0
  193. data/spec/integration/end_to_end_spec.rb +52 -16
  194. data/spec/integration/generators/automation_generator_spec.rb +0 -12
  195. data/spec/integration/generators/axe_addon_spec.rb +150 -0
  196. data/spec/integration/generators/common_generator_spec.rb +12 -13
  197. data/spec/integration/generators/config_features_spec.rb +155 -0
  198. data/spec/integration/generators/debug_helper_spec.rb +68 -0
  199. data/spec/integration/generators/helpers_generator_spec.rb +0 -12
  200. data/spec/integration/generators/lighthouse_addon_spec.rb +132 -0
  201. data/spec/integration/generators/minitest_generator_spec.rb +0 -6
  202. data/spec/integration/generators/reporter_spec.rb +159 -0
  203. data/spec/integration/generators/skip_flags_spec.rb +134 -0
  204. data/spec/integration/generators/visual_addon_spec.rb +148 -0
  205. data/spec/integration/scaffolding_e2e_spec.rb +775 -0
  206. data/spec/integration/settings_helper.rb +0 -3
  207. data/spec/integration/spec_helper.rb +30 -13
  208. data/spec/llm/client_spec.rb +79 -0
  209. data/spec/llm/config_spec.rb +92 -0
  210. data/spec/llm/prompts_spec.rb +49 -0
  211. data/spec/llm/response_parser_spec.rb +92 -0
  212. data/spec/menus/adopter_adopt_menu_spec.rb +97 -0
  213. data/spec/menus/menu_generator_spec.rb +263 -0
  214. data/spec/scaffolding/name_normalizer_spec.rb +113 -0
  215. data/spec/scaffolding/page_introspector_spec.rb +82 -0
  216. data/spec/scaffolding/scaffold_project_detector_spec.rb +142 -0
  217. data/spec/scaffolding/scaffolding_features_spec.rb +311 -0
  218. data/spec/scaffolding/url_analyzer_spec.rb +110 -0
  219. data/spec/system/adopt_matrix_spec.rb +537 -0
  220. data/spec/system/adopt_spec.rb +225 -0
  221. data/spec/system/support/system_test_helper.rb +0 -2
  222. data/spec/utilities/desktop_downloader_spec.rb +92 -0
  223. data/spec/utilities/headless_config_spec.rb +89 -0
  224. data/spec/utilities/utilities_spec.rb +105 -0
  225. metadata +154 -5
  226. data/lib/generators/automation/templates/visual_options.tt +0 -16
  227. data/lib/generators/templates/helpers/partials/axe_driver.tt +0 -10
  228. data/lib/generators/templates/helpers/visual_spec_helper.tt +0 -35
@@ -1,6 +1,3 @@
1
- # :reek:FeatureEnvy { enabled: false }
2
- # :reek:UtilityFunction { enabled: false }
3
-
4
1
  module SettingsHelper
5
2
  def create_settings(options)
6
3
  automation = options[:automation]
@@ -5,7 +5,7 @@ require 'rspec'
5
5
  require_relative '../../lib/generators/invoke_generators'
6
6
  require_relative 'settings_helper'
7
7
 
8
- AUTOMATION_TYPES = %w[android ios selenium watir cross_platform axe applitools capybara].freeze
8
+ AUTOMATION_TYPES = %w[android ios selenium watir cross_platform capybara].freeze
9
9
  FRAMEWORKS = %w[cucumber rspec minitest].freeze
10
10
  CI_PLATFORMS = [nil, 'github', 'gitlab'].freeze
11
11
 
@@ -22,36 +22,53 @@ module AutomationIndex
22
22
  SELENIUM = AUTOMATION_TYPES[2]
23
23
  WATIR = AUTOMATION_TYPES[3]
24
24
  CROSS_PLATFORM = AUTOMATION_TYPES[4]
25
- AXE = AUTOMATION_TYPES[5]
26
- APPLITOOLS = AUTOMATION_TYPES[6]
27
- CAPYBARA = AUTOMATION_TYPES[7]
25
+ CAPYBARA = AUTOMATION_TYPES[5]
26
+ end
27
+
28
+ # Lazy project generation — only generates a project the first time it's needed.
29
+ # Replaces the old before(:all) that generated all 54 combinations upfront.
30
+ module LazyProjectGenerator
31
+ GENERATED = Set.new
32
+ MUTEX = Mutex.new
33
+
34
+ def ensure_project(framework:, automation:, ci_platform: nil)
35
+ settings = create_settings(framework:, automation:, ci_platform:)
36
+ project_name = settings[:name]
37
+
38
+ MUTEX.synchronize do
39
+ unless GENERATED.include?(project_name)
40
+ generate_framework(settings)
41
+ GENERATED.add(project_name)
42
+ end
43
+ end
44
+
45
+ project_name
46
+ end
28
47
  end
29
48
 
30
49
  RSpec.configure do |config|
31
50
  config.include(InvokeGenerators)
32
51
  config.include(SettingsHelper)
52
+ config.include(LazyProjectGenerator)
33
53
 
34
54
  # rubocop:disable RSpec/BeforeAfterAll
35
55
  config.before(:all) do
56
+ # Generate all projects upfront (preserves existing behavior).
57
+ # Individual specs can also call ensure_project() for on-demand generation.
36
58
  FRAMEWORKS.each do |framework|
37
59
  AUTOMATION_TYPES.each do |automation|
38
60
  CI_PLATFORMS.each do |ci_platform|
39
- settings = create_settings(framework:, automation:, ci_platform:)
40
- generate_framework(settings)
61
+ ensure_project(framework:, automation:, ci_platform:)
41
62
  end
42
63
  end
43
64
  end
44
65
  end
45
66
 
46
67
  config.after(:all) do
47
- FRAMEWORKS.each do |framework|
48
- AUTOMATION_TYPES.each do |automation|
49
- CI_PLATFORMS.each do |ci_platform|
50
- settings = create_settings(framework:, automation:, ci_platform:)
51
- FileUtils.rm_rf(settings[:name])
52
- end
53
- end
68
+ LazyProjectGenerator::GENERATED.each do |project_name|
69
+ FileUtils.rm_rf(project_name)
54
70
  end
71
+ LazyProjectGenerator::GENERATED.clear
55
72
  end
56
73
  # rubocop:enable RSpec/BeforeAfterAll
57
74
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/llm/client'
4
+ require_relative '../../lib/llm/provider'
5
+
6
+ RSpec.describe Llm::Client do
7
+ let(:mock_provider) { instance_double(Llm::Provider, available?: true) }
8
+ let(:mock_config) { instance_double(Llm::Config, configured?: true, provider_name: 'ollama', model: 'llama3.2') }
9
+
10
+ before do
11
+ allow(Llm::Config).to receive(:new).and_return(mock_config)
12
+ allow(mock_config).to receive(:build_provider).and_return(mock_provider)
13
+ end
14
+
15
+ describe '.complete' do
16
+ it 'delegates to the provider' do
17
+ allow(mock_provider).to receive(:complete).and_return('AI response')
18
+ result = described_class.complete('test prompt')
19
+ expect(result).to eq('AI response')
20
+ end
21
+
22
+ it 'returns nil when not configured' do
23
+ allow(mock_config).to receive(:configured?).and_return(false)
24
+ expect(described_class.complete('test prompt')).to be_nil
25
+ end
26
+
27
+ it 'returns nil when provider is not available' do
28
+ allow(mock_provider).to receive(:available?).and_return(false)
29
+ expect(described_class.complete('test prompt')).to be_nil
30
+ end
31
+
32
+ it 'retries on failure and returns nil after max retries' do
33
+ allow(mock_provider).to receive(:complete).and_raise(StandardError, 'network error')
34
+ allow(described_class).to receive(:sleep) # skip actual sleep
35
+ expect(described_class.complete('test prompt')).to be_nil
36
+ end
37
+
38
+ it 'passes system_prompt to provider' do
39
+ allow(mock_provider).to receive(:complete).and_return('ok')
40
+ result = described_class.complete('prompt', system_prompt: 'system')
41
+ expect(result).to eq('ok')
42
+ expect(mock_provider).to have_received(:complete).with('prompt', system_prompt: 'system')
43
+ end
44
+ end
45
+
46
+ describe '.available?' do
47
+ it 'returns true when provider is configured and available' do
48
+ expect(described_class).to be_available
49
+ end
50
+
51
+ it 'returns false when not configured' do
52
+ allow(mock_config).to receive(:configured?).and_return(false)
53
+ expect(described_class).not_to be_available
54
+ end
55
+
56
+ it 'returns false when provider is not available' do
57
+ allow(mock_provider).to receive(:available?).and_return(false)
58
+ expect(described_class).not_to be_available
59
+ end
60
+ end
61
+
62
+ describe '.status' do
63
+ it 'returns configured status with details' do
64
+ allow(mock_provider).to receive(:available?).and_return(true)
65
+ status = described_class.status
66
+ expect(status[:configured]).to be true
67
+ expect(status[:provider]).to eq('ollama')
68
+ expect(status[:model]).to eq('llama3.2')
69
+ expect(status[:available]).to be true
70
+ end
71
+
72
+ it 'returns unconfigured status' do
73
+ allow(mock_config).to receive(:configured?).and_return(false)
74
+ status = described_class.status
75
+ expect(status[:configured]).to be false
76
+ expect(status[:provider]).to be_nil
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/llm/config'
4
+
5
+ RSpec.describe Llm::Config do
6
+ before do
7
+ allow(ENV).to receive(:fetch).and_call_original
8
+ %w[RUBY_RAIDER_LLM_PROVIDER RUBY_RAIDER_LLM_API_KEY RUBY_RAIDER_LLM_MODEL RUBY_RAIDER_LLM_URL].each do |key|
9
+ allow(ENV).to receive(:fetch).with(key, nil).and_return(nil)
10
+ end
11
+ allow(File).to receive(:exist?).with('config/config.yml').and_return(false)
12
+ end
13
+
14
+ describe '#configured?' do
15
+ it 'returns false when no provider is set' do
16
+ expect(described_class.new).not_to be_configured
17
+ end
18
+
19
+ it 'returns false for unknown provider' do
20
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('unknown')
21
+ expect(described_class.new).not_to be_configured
22
+ end
23
+
24
+ it 'returns true for ollama without api key' do
25
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('ollama')
26
+ expect(described_class.new).to be_configured
27
+ end
28
+
29
+ it 'returns false for openai without api key' do
30
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('openai')
31
+ expect(described_class.new).not_to be_configured
32
+ end
33
+
34
+ it 'returns true for openai with api key' do
35
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('openai')
36
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_API_KEY', nil).and_return('sk-test')
37
+ expect(described_class.new).to be_configured
38
+ end
39
+
40
+ it 'returns true for anthropic with api key' do
41
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('anthropic')
42
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_API_KEY', nil).and_return('sk-ant-test')
43
+ expect(described_class.new).to be_configured
44
+ end
45
+ end
46
+
47
+ describe '#build_provider' do
48
+ it 'returns nil when not configured' do
49
+ expect(described_class.new.build_provider).to be_nil
50
+ end
51
+
52
+ it 'builds OpenaiProvider for openai' do
53
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('openai')
54
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_API_KEY', nil).and_return('sk-test')
55
+ provider = described_class.new.build_provider
56
+ expect(provider).to be_a(Llm::Providers::OpenaiProvider)
57
+ end
58
+
59
+ it 'builds AnthropicProvider for anthropic' do
60
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('anthropic')
61
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_API_KEY', nil).and_return('sk-ant-test')
62
+ provider = described_class.new.build_provider
63
+ expect(provider).to be_a(Llm::Providers::AnthropicProvider)
64
+ end
65
+
66
+ it 'builds OllamaProvider for ollama' do
67
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('ollama')
68
+ provider = described_class.new.build_provider
69
+ expect(provider).to be_a(Llm::Providers::OllamaProvider)
70
+ end
71
+ end
72
+
73
+ describe 'config file fallback' do
74
+ it 'reads from config/config.yml when env vars are not set' do
75
+ allow(File).to receive(:exist?).with('config/config.yml').and_return(true)
76
+ allow(YAML).to receive(:load_file).with('config/config.yml').and_return(
77
+ 'llm_provider' => 'ollama',
78
+ 'llm_model' => 'codellama'
79
+ )
80
+ config = described_class.new
81
+ expect(config.provider_name).to eq('ollama')
82
+ expect(config.model).to eq('codellama')
83
+ end
84
+
85
+ it 'env vars take precedence over config file' do
86
+ allow(ENV).to receive(:fetch).with('RUBY_RAIDER_LLM_PROVIDER', nil).and_return('anthropic')
87
+ allow(File).to receive(:exist?).with('config/config.yml').and_return(true)
88
+ allow(YAML).to receive(:load_file).with('config/config.yml').and_return('llm_provider' => 'ollama')
89
+ expect(described_class.new.provider_name).to eq('anthropic')
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/llm/prompts'
4
+
5
+ RSpec.describe Llm::Prompts do
6
+ describe '.analyze_page' do
7
+ it 'includes URL and HTML in prompt' do
8
+ prompt = described_class.analyze_page('<html>test</html>', 'https://example.com')
9
+ expect(prompt).to include('https://example.com')
10
+ expect(prompt).to include('<html>test</html>')
11
+ end
12
+
13
+ it 'truncates HTML at 8000 characters' do
14
+ long_html = 'x' * 10_000
15
+ prompt = described_class.analyze_page(long_html, 'https://example.com')
16
+ expect(prompt.length).to be < 10_000
17
+ end
18
+
19
+ it 'requests JSON output' do
20
+ prompt = described_class.analyze_page('<html></html>', 'https://example.com')
21
+ expect(prompt).to include('JSON')
22
+ expect(prompt).to include('elements')
23
+ end
24
+ end
25
+
26
+ describe '.generate_test_scenarios' do
27
+ let(:methods) { [{ name: 'login', params: %w[user pass] }, { name: 'logout', params: [] }] }
28
+
29
+ it 'includes class and method info' do
30
+ prompt = described_class.generate_test_scenarios('LoginPage', methods, 'selenium', 'rspec')
31
+ expect(prompt).to include('LoginPage')
32
+ expect(prompt).to include('login(user, pass)')
33
+ expect(prompt).to include('logout')
34
+ end
35
+
36
+ it 'includes automation and framework context' do
37
+ prompt = described_class.generate_test_scenarios('LoginPage', methods, 'selenium', 'rspec')
38
+ expect(prompt).to include('selenium')
39
+ expect(prompt).to include('rspec')
40
+ end
41
+ end
42
+
43
+ describe '.system_prompt' do
44
+ it 'returns a non-empty string' do
45
+ expect(described_class.system_prompt).to be_a(String)
46
+ expect(described_class.system_prompt).not_to be_empty
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/llm/response_parser'
4
+
5
+ RSpec.describe Llm::ResponseParser do
6
+ describe '.parse_json' do
7
+ it 'parses raw JSON' do
8
+ result = described_class.parse_json('{"key": "value"}')
9
+ expect(result).to eq(key: 'value')
10
+ end
11
+
12
+ it 'parses JSON from markdown code block' do
13
+ response = "```json\n{\"key\": \"value\"}\n```"
14
+ result = described_class.parse_json(response)
15
+ expect(result).to eq(key: 'value')
16
+ end
17
+
18
+ it 'extracts JSON from surrounding text' do
19
+ response = "Here is the result:\n{\"key\": \"value\"}\nDone."
20
+ result = described_class.parse_json(response)
21
+ expect(result).to eq(key: 'value')
22
+ end
23
+
24
+ it 'returns nil for nil input' do
25
+ expect(described_class.parse_json(nil)).to be_nil
26
+ end
27
+
28
+ it 'returns nil for empty input' do
29
+ expect(described_class.parse_json('')).to be_nil
30
+ end
31
+
32
+ it 'returns nil for invalid JSON' do
33
+ expect(described_class.parse_json('not json at all')).to be_nil
34
+ end
35
+
36
+ it 'returns nil for JSON arrays (not hashes)' do
37
+ expect(described_class.parse_json('[1, 2, 3]')).to be_nil
38
+ end
39
+ end
40
+
41
+ describe '.extract_elements' do
42
+ let(:valid_response) do
43
+ {
44
+ elements: [
45
+ { name: 'email_field', type: 'input', locator: { type: 'id', value: 'email' }, purpose: 'Email input' },
46
+ { name: 'submit_btn', type: 'button', locator: { type: 'id', value: 'submit' }, text: 'Submit' }
47
+ ]
48
+ }.to_json
49
+ end
50
+
51
+ it 'extracts and normalizes elements' do
52
+ elements = described_class.extract_elements(valid_response)
53
+ expect(elements.length).to eq(2)
54
+ expect(elements.first[:name]).to eq('email_field')
55
+ expect(elements.first[:locator][:type]).to eq(:id)
56
+ end
57
+
58
+ it 'skips elements without required fields' do
59
+ response = { elements: [{ name: 'incomplete' }] }.to_json
60
+ elements = described_class.extract_elements(response)
61
+ expect(elements).to be_empty
62
+ end
63
+
64
+ it 'returns nil for non-element response' do
65
+ expect(described_class.extract_elements('{"other": "data"}')).to be_nil
66
+ end
67
+
68
+ it 'returns nil for nil response' do
69
+ expect(described_class.extract_elements(nil)).to be_nil
70
+ end
71
+ end
72
+
73
+ describe '.extract_scenarios' do
74
+ let(:valid_response) do
75
+ {
76
+ scenarios: [
77
+ { method: 'login', description: 'logs in with valid credentials', assertion_hint: 'expect redirect' }
78
+ ]
79
+ }.to_json
80
+ end
81
+
82
+ it 'extracts scenarios' do
83
+ scenarios = described_class.extract_scenarios(valid_response)
84
+ expect(scenarios.length).to eq(1)
85
+ expect(scenarios.first[:method]).to eq('login')
86
+ end
87
+
88
+ it 'returns nil for non-scenario response' do
89
+ expect(described_class.extract_scenarios('{"other": "data"}')).to be_nil
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'rspec'
5
+ require_relative '../../lib/adopter/adopt_menu'
6
+
7
+ RSpec.describe Adopter::AdoptMenu do # rubocop:disable RSpec/FilePath
8
+ let(:prompt) { instance_double(TTY::Prompt) }
9
+ let(:source_dir) { 'tmp_adopt_interactive_source' }
10
+ let(:output_dir) { 'tmp_adopt_interactive_output' }
11
+
12
+ before do
13
+ allow(TTY::Prompt).to receive(:new).and_return(prompt)
14
+ allow(prompt).to receive(:say)
15
+ allow(prompt).to receive(:warn)
16
+ allow(prompt).to receive(:error)
17
+
18
+ FileUtils.mkdir_p(source_dir)
19
+ File.write("#{source_dir}/Gemfile", "gem 'rspec'\ngem 'selenium-webdriver'\n")
20
+ FileUtils.mkdir_p("#{source_dir}/spec")
21
+ File.write("#{source_dir}/spec/login_spec.rb", "describe 'Login' do\n it('works') { expect(true).to be true }\nend\n")
22
+ end
23
+
24
+ after do
25
+ FileUtils.rm_rf(source_dir)
26
+ FileUtils.rm_rf(output_dir)
27
+ end
28
+
29
+ describe '#run' do
30
+ before do
31
+ allow(prompt).to receive(:ask).and_return(source_dir, output_dir)
32
+ allow(prompt).to receive(:select)
33
+ .with('Select target automation framework:', anything, anything).and_return('Selenium')
34
+ allow(prompt).to receive(:select)
35
+ .with('Select target test framework:', anything, anything).and_return('Rspec')
36
+ allow(prompt).to receive(:select)
37
+ .with('Configure CI/CD?', anything, anything).and_return(nil)
38
+ end
39
+
40
+ it 'shows detection preview' do
41
+ described_class.new.run
42
+
43
+ expect(prompt).to have_received(:say).with(match(/Detected project settings/))
44
+ expect(prompt).to have_received(:say).with(match(/Automation: selenium/))
45
+ expect(prompt).to have_received(:say).with(match(/Framework:.*rspec/))
46
+ end
47
+
48
+ it 'executes adoption and shows results' do
49
+ described_class.new.run
50
+
51
+ expect(prompt).to have_received(:say).with(match(/Adoption complete/))
52
+ expect(prompt).to have_received(:say).with(match(/Pages converted/))
53
+ expect(prompt).to have_received(:say).with(match(/Tests converted/))
54
+ end
55
+
56
+ it 'generates the output project' do
57
+ described_class.new.run
58
+
59
+ expect(File).to exist("#{output_dir}/Gemfile")
60
+ end
61
+
62
+ it 'shows output path and bundle install instruction' do
63
+ described_class.new.run
64
+
65
+ expect(prompt).to have_received(:say).with(match(/Output: #{output_dir}/))
66
+ expect(prompt).to have_received(:say).with(match(/bundle install/))
67
+ end
68
+ end
69
+
70
+ describe '#run with mobile source project' do
71
+ before do
72
+ File.write("#{source_dir}/Gemfile", "gem 'rspec'\ngem 'appium_lib'\n")
73
+ allow(prompt).to receive(:ask).and_return(source_dir, output_dir)
74
+ allow(prompt).to receive(:select).and_return('Selenium', 'Rspec', nil)
75
+ end
76
+
77
+ it 'shows MobileProjectError message' do
78
+ described_class.new.run
79
+
80
+ expect(prompt).to have_received(:error).with(match(/Mobile.*Appium.*cannot be adopted/))
81
+ end
82
+ end
83
+
84
+ describe 'validation constants' do
85
+ it 'defines web automations' do
86
+ expect(described_class::WEB_AUTOMATIONS).to contain_exactly('selenium', 'capybara', 'watir')
87
+ end
88
+
89
+ it 'defines test frameworks' do
90
+ expect(described_class::TEST_FRAMEWORKS).to contain_exactly('rspec', 'cucumber', 'minitest')
91
+ end
92
+
93
+ it 'defines CI platforms' do
94
+ expect(described_class::CI_PLATFORMS).to contain_exactly('github', 'gitlab')
95
+ end
96
+ end
97
+ end