bddgenx 0.1.51 → 2.0.5

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 887584d64b29e9d2eecbedefa011eb4c3cd2c1e7ee8e3e71b2f2d602b9229f34
4
- data.tar.gz: f58a637a706bb935d3bda39078cfcf22773612a9857bbe86a9af395ca0e7f468
3
+ metadata.gz: 0ee7808e2b0f0cf6e65b1dbc5ea11689a1522dbc06ec2db6ed35494329920913
4
+ data.tar.gz: f88f37264c7e26caa90f4b25aef60e194cd5b08d7a97819b4aa623a7e99d6b04
5
5
  SHA512:
6
- metadata.gz: 2c0a0dc0277a314964853a723559ea784d0a660de28d2bba7ea3d94c6cc4833df35b76c8d3f09d81896c801c2bd9baf6668efb3afe0ab8599bf7d45588d5cc48
7
- data.tar.gz: 804cefd2b537ca4f9902a1790b702c7d79f457b5b94e6316d6b5c9793c5d6cafd358e36c999c7ed3804820c34138b938bede09687a1eea0486280b356b5168cf
6
+ metadata.gz: 1bdc7124701b3fd7eba6e60993e75e31fb19613a1ae53cb7549afeae1c0d4a7f167a3e5c19bf6805ee118ffd6a6c7a6b5d29b0d25e34e894dbfb86c0e45f2879
7
+ data.tar.gz: eec2871ad622cb5c638163854dbb7dabcb5da57e6bb784732c561c50cc1a4fabc81c3647b82c63509a6bd125e72801db2a69a88415d550af44ccf9f944ed7ac7
data/README.md CHANGED
@@ -5,6 +5,56 @@
5
5
 
6
6
  Ferramenta Ruby para gerar automaticamente arquivos Gherkin (`.feature`) e definições de passos (`steps.rb`) a partir de histórias em texto. Atende aos padrões ISTQB, suporta parametrização com blocos de exemplos e fornece relatórios de QA (rastreabilidade, backups e PDF).
7
7
 
8
+ ## Estrutura do projeto
9
+ ```
10
+ bdd-generation/
11
+ ├── .github/ # Workflows do GitHub Actions
12
+ │ └── workflows/
13
+ │ └── main.yml
14
+ ├── bin/
15
+ │ └── console # Execução local (se necessário)
16
+ ├── features/ # Gherkin gerados automaticamente
17
+ │ └── steps/ # Steps correspondentes
18
+ ├── input/ # Histórias de entrada
19
+ │ ├── historia.txt
20
+ │ ├── historia_en.txt
21
+ │ └── ...
22
+ ├── lib/
23
+ │ └── bddgenx/
24
+ │ ├── ia/ # Módulo de IA
25
+ │ │ ├── chatgpt_cliente.rb
26
+ │ │ └── gemini_cliente.rb
27
+ │ ├── generators/ # Responsável por geração
28
+ │ │ ├── generator.rb
29
+ │ │ ├── steps_generator.rb
30
+ │ │ └── runner.rb
31
+ │ ├── reports/ # Exportadores, backups e rastreabilidade
32
+ │ │ ├── pdf_exporter.rb
33
+ │ │ ├── backup.rb
34
+ │ │ └── tracer.rb
35
+ │ ├── support/ # Utilitários auxiliares
36
+ │ │ ├── gherkin_cleaner.rb
37
+ │ │ ├── remover_steps_duplicados.rb
38
+ │ │ ├── validator.rb
39
+ │ │ └── font_loader.rb
40
+ │ ├── parser.rb # Parse do .txt
41
+ │ ├── version.rb # Versão da gem
42
+ │ └── bddgenx.rb # Arquivo principal (require central)
43
+ ├── reports/ # Saídas: PDF, backup, rastreabilidade
44
+ │ ├── pdf/
45
+ │ ├── backup/
46
+ │ └── rastreabilidade/
47
+ ├── .env # Variáveis de ambiente
48
+ ├── .gitignore
49
+ ├── bddgenx.gemspec
50
+ ├── bump_version.sh
51
+ ├── Gemfile
52
+ ├── Gemfile.lock
53
+ ├── LICENSE
54
+ ├── Rakefile
55
+ ├── README.md
56
+ └── VERSION
57
+ ```
8
58
  ## Instalação
9
59
 
10
60
  Adicione ao seu `Gemfile`:
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.51
1
+ 2.0.5
data/bin/bddgenx CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- require_relative "../lib/bddgenx/runner"
2
+ require_relative "../lib/bddgenx/generators/runner"
3
3
 
4
4
  Bddgenx::Runner.execute
@@ -0,0 +1,175 @@
1
+ # lib/bddgenx/generator.rb
2
+ # encoding: utf-8
3
+ #
4
+ # Este arquivo define a classe Generator, responsável por gerar arquivos
5
+ # .feature a partir de um hash de história ou de um arquivo de história em texto.
6
+ # Suporta Gherkin em Português e Inglês, inclusão de tags, cenários simples
7
+ # e esquemas de cenário com exemplos.
8
+
9
+ module Bddgenx
10
+ class Generator
11
+ # Palavras-chave do Gherkin em Português
12
+ GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
13
+
14
+ # Palavras-chave do Gherkin em Inglês
15
+ GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
16
+
17
+ # Mapeamento de PT → EN
18
+ GHERKIN_MAP_PT_EN = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
19
+
20
+ # Mapeamento de EN → PT
21
+ GHERKIN_MAP_EN_PT = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
22
+
23
+ # Todas as palavras-chave reconhecidas
24
+ ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
25
+
26
+ ##
27
+ # Extrai todas as linhas de exemplo de um array de strings.
28
+ #
29
+ # @param raw [Array<String>] um array contendo linhas de texto
30
+ # @return [Array<String>] apenas as linhas que começam com '|', ou seja, exemplos
31
+ def self.dividir_examples(raw)
32
+ raw.select { |l| l.strip.start_with?('|') }
33
+ end
34
+
35
+ ##
36
+ # Gera o conteúdo de um arquivo `.feature` a partir de uma história.
37
+ # Pode operar em três modos: estático (hash ou arquivo estruturado),
38
+ # IA com Gemini, ou IA com ChatGPT.
39
+ #
40
+ # @param input [String, Hash] caminho para um arquivo .txt ou um hash estruturado
41
+ # @param override_path [String, nil] caminho alternativo para salvar o arquivo gerado
42
+ # @return [Array(String, String)] caminho do arquivo gerado e conteúdo do .feature
43
+ def self.gerar_feature(input, override_path = nil)
44
+ modo = ENV['BDD_MODE']&.to_sym || :static
45
+
46
+ if input.is_a?(String) && input.end_with?('.txt') && [:gemini, :chatgpt].include?(modo)
47
+ # Modo com IA: gera cenários automaticamente com base no texto da história
48
+ raw_txt = File.read(input)
49
+ historia = {
50
+ idioma: 'pt',
51
+ quero: File.basename(input, '.txt').tr('_', ' ').capitalize,
52
+ como: '',
53
+ para: '',
54
+ grupos: []
55
+ }
56
+
57
+ texto_gerado = if modo == :gemini
58
+ GeminiCliente.gerar_cenarios(raw_txt)
59
+ else
60
+ ChatGPTCliente.gerar_cenarios(raw_txt)
61
+ end
62
+
63
+ historia[:grupos] << {
64
+ tipo: 'gerado',
65
+ tag: 'ia',
66
+ passos: GherkinCleaner.limpar(texto_gerado).lines.map(&:strip).reject(&:empty?)
67
+ }
68
+ else
69
+ # Modo estático: utiliza estrutura vinda do Parser ou de um hash diretamente
70
+ historia = input.is_a?(String) ? Parser.ler_historia(input) : input
71
+ end
72
+
73
+ idioma = historia[:idioma] || 'pt'
74
+ cont = 1
75
+
76
+ # Normaliza o nome base do arquivo
77
+ nome_base = historia[:quero]
78
+ .gsub(/[^a-z0-9]/i, '_')
79
+ .downcase
80
+ .split('_')
81
+ .reject(&:empty?)
82
+ .first(5)
83
+ .join('_')
84
+
85
+ caminho = override_path || "features/#{nome_base}.feature"
86
+
87
+ # Define palavras-chave com base no idioma
88
+ palavras = {
89
+ feature: idioma == 'en' ? 'Feature' : 'Funcionalidade',
90
+ contexto: idioma == 'en' ? 'Background' : 'Contexto',
91
+ cenario: idioma == 'en' ? 'Scenario' : 'Cenário',
92
+ esquema: idioma == 'en' ? 'Scenario Outline' : 'Esquema do Cenário',
93
+ exemplos: idioma == 'en' ? 'Examples' : 'Exemplos',
94
+ regra: idioma == 'en' ? 'Rule' : 'Regra'
95
+ }
96
+
97
+ conteudo = <<~GHK
98
+ # language: #{idioma}
99
+ #{palavras[:feature]}: #{historia[:quero].sub(/^Quero\s*/i,'')}
100
+ # #{historia[:como]}
101
+ # #{historia[:quero]}
102
+ # #{historia[:para]}
103
+ GHK
104
+
105
+ passos_unicos = Set.new
106
+ pt_map = GHERKIN_MAP_PT_EN
107
+ en_map = GHERKIN_MAP_EN_PT
108
+ detect = ALL_KEYS
109
+ cont = 1
110
+
111
+ historia[:grupos].each do |grupo|
112
+ passos = grupo[:passos] || []
113
+ exemplos = grupo[:exemplos] || []
114
+ next if passos.empty?
115
+
116
+ tag_line = ["@#{grupo[:tipo].downcase}", ("@#{grupo[:tag]}" if grupo[:tag])].compact.join(' ')
117
+ conteudo << " #{tag_line}\n"
118
+
119
+ if exemplos.any?
120
+ conteudo << " #{palavras[:esquema]}: Exemplo #{cont}\n"
121
+ cont += 1
122
+ else
123
+ conteudo << " #{palavras[:cenario]}: #{grupo[:tipo].capitalize}\n"
124
+ end
125
+
126
+ passos.each do |p|
127
+ parts = p.strip.split(' ', 2)
128
+ con_in = detect.find { |k| k.casecmp(parts[0]) == 0 } || parts[0]
129
+ text = parts[1] || ''
130
+ out_conn = idioma == 'en' ? pt_map[con_in] || con_in : en_map[con_in] || con_in
131
+ linha_step = " #{out_conn} #{text}"
132
+
133
+ next if passos_unicos.include?(linha_step)
134
+
135
+ passos_unicos << linha_step
136
+ conteudo << "#{linha_step}\n"
137
+ end
138
+
139
+ if exemplos.any?
140
+ conteudo << "\n #{palavras[:exemplos]}:\n"
141
+ exemplos.select { |l| l.strip.start_with?('|') }.each do |line|
142
+ cleaned = line.strip.gsub(/^"|"$/, '')
143
+ conteudo << " #{cleaned}\n"
144
+ end
145
+ end
146
+
147
+ conteudo << "\n"
148
+ end
149
+
150
+ [caminho, conteudo]
151
+ end
152
+
153
+ ##
154
+ # Gera o caminho padrão de saída para um arquivo `.feature` com base no nome do `.txt`.
155
+ #
156
+ # @param arquivo_txt [String] caminho do arquivo .txt de entrada
157
+ # @return [String] caminho completo do arquivo .feature correspondente
158
+ def self.path_para_feature(arquivo_txt)
159
+ nome = File.basename(arquivo_txt, '.txt')
160
+ File.join('features', "#{nome}.feature")
161
+ end
162
+
163
+ ##
164
+ # Salva o conteúdo gerado no disco, criando diretórios se necessário.
165
+ #
166
+ # @param caminho [String] caminho completo para salvar o arquivo
167
+ # @param conteudo [String] conteúdo do arquivo .feature
168
+ # @return [void]
169
+ def self.salvar_feature(caminho, conteudo)
170
+ FileUtils.mkdir_p(File.dirname(caminho))
171
+ File.write(caminho, conteudo)
172
+ puts "✅ Arquivo .feature gerado: #{caminho}"
173
+ end
174
+ end
175
+ end
@@ -4,14 +4,7 @@
4
4
  # Este arquivo define a classe Runner (CLI) da gem bddgenx,
5
5
  # responsável por orquestrar o fluxo de leitura de histórias,
6
6
  # validação, geração de features, steps, backups e exportação de PDFs.
7
-
8
- require 'fileutils'
9
- require_relative 'utils/parser'
10
- require_relative 'generator'
11
- require_relative 'utils/pdf_exporter'
12
- require_relative 'steps_generator'
13
- require_relative 'utils/validator'
14
- require_relative 'utils/backup'
7
+ require_relative '../../bddgenx'
15
8
 
16
9
  module Bddgenx
17
10
  # Ponto de entrada da gem: coordena todo o processo de geração BDD.
@@ -80,6 +73,8 @@ module Bddgenx
80
73
  #
81
74
  # @return [void]
82
75
  def self.execute
76
+ modo = ENV['BDDGENX_MODE'] || 'static'
77
+
83
78
  input_dir = 'input'
84
79
  Dir.mkdir(input_dir) unless Dir.exist?(input_dir)
85
80
 
@@ -106,7 +101,27 @@ module Bddgenx
106
101
  end
107
102
 
108
103
  # Geração de feature
109
- feature_path, feature_content = Generator.gerar_feature(historia)
104
+ if modo == 'gemini' || modo == 'chatgpt'
105
+ puts "🤖 Gerando cenários com IA (#{modo.capitalize})..."
106
+ idioma = IA::GeminiCliente.detecta_idioma_arquivo(arquivo)
107
+ feature_text =
108
+ if modo == 'gemini'
109
+ IA::GeminiCliente.gerar_cenarios(historia, idioma)
110
+ else
111
+ IA::ChatGptCliente.gerar_cenarios(historia, idioma)
112
+ end
113
+ if feature_text
114
+ feature_path = Generator.path_para_feature(arquivo)
115
+ feature_content = Bddgenx::GherkinCleaner.limpar(feature_text)
116
+ else
117
+ ignored += 1
118
+ puts "❌ Falha ao gerar com IA: #{arquivo}"
119
+ next
120
+ end
121
+ else
122
+ feature_path, feature_content = Generator.gerar_feature(historia)
123
+ end
124
+
110
125
  Backup.salvar_versao_antiga(feature_path)
111
126
  features += 1 if Generator.salvar_feature(feature_path, feature_content)
112
127
 
@@ -6,82 +6,77 @@
6
6
  # Suporta palavras-chave Gherkin em Português e Inglês e parametriza
7
7
  # strings e números conforme necessário.
8
8
 
9
- require 'fileutils'
10
- require 'strscan' # Para uso de StringScanner
11
-
12
9
  module Bddgenx
13
- # Gera arquivos de definições de passos Ruby para Cucumber
14
- # com base em arquivos .feature.
15
10
  class StepsGenerator
16
- # Palavras-chave Gherkin em Português usadas em arquivos de feature
17
- # @return [Array<String>]
11
+ # Palavras-chave Gherkin em Português
18
12
  GHERKIN_KEYS_PT = %w[Dado Quando Então E Mas].freeze
19
13
 
20
- # Palavras-chave Gherkin em Inglês usadas em arquivos de feature
21
- # @return [Array<String>]
14
+ # Palavras-chave Gherkin em Inglês
22
15
  GHERKIN_KEYS_EN = %w[Given When Then And But].freeze
23
16
 
24
- # Conjunto de todas as palavras-chave suportadas (PT + EN)
25
- # @return [Array<String>]
17
+ # Conjunto de todas as palavras-chave suportadas
26
18
  ALL_KEYS = GHERKIN_KEYS_PT + GHERKIN_KEYS_EN
27
19
 
28
- # Converte uma string para camelCase, útil para nomes de argumentos
20
+ ##
21
+ # Transforma uma string em estilo camelCase.
22
+ #
23
+ # @param str [String] A string a ser transformada.
24
+ # @return [String] A string convertida para camelCase.
29
25
  #
30
- # @param [String] str Texto de entrada a ser convertido
31
- # @return [String] Versão em camelCase do texto
32
26
  def self.camelize(str)
33
27
  partes = str.strip.split(/[^a-zA-Z0-9]+/)
34
28
  partes.map.with_index { |palavra, i| i.zero? ? palavra.downcase : palavra.capitalize }.join
35
29
  end
36
30
 
37
- # Gera definições de passos a partir de um arquivo .feature
31
+ ##
32
+ # Gera arquivos de passos do Cucumber a partir de um arquivo .feature.
33
+ #
34
+ # O método lê o arquivo, detecta o idioma, extrai os passos,
35
+ # parametriza as variáveis (números e strings), e escreve os métodos
36
+ # em um novo arquivo no diretório `steps/`.
37
+ #
38
+ # @param feature_path [String] Caminho para o arquivo .feature.
39
+ # @return [Boolean] Retorna true se os passos forem gerados com sucesso, false se não houver passos.
40
+ # @raise [ArgumentError] Se o caminho fornecido não for uma String.
38
41
  #
39
- # @param [String] feature_path Caminho para o arquivo .feature
40
- # @raise [ArgumentError] Se feature_path não for String
41
- # @return [Boolean] Retorna true se passos foram gerados, false se não houver passos
42
42
  def self.gerar_passos(feature_path)
43
- # Valida tipo de entrada
44
- unless feature_path.is_a?(String)
45
- raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}"
46
- end
43
+ raise ArgumentError, "Caminho esperado como String, recebeu #{feature_path.class}" unless feature_path.is_a?(String)
47
44
 
48
45
  linhas = File.readlines(feature_path)
49
46
 
50
- # Detecta idioma no cabeçalho: "# language: pt" ou "# language: en"
47
+ # Detecta o idioma com base na diretiva "# language:"
51
48
  lang = if (m = linhas.find { |l| l =~ /^#\s*language:\s*(\w+)/i })
52
49
  m[/^#\s*language:\s*(\w+)/i, 1].downcase
53
50
  else
54
51
  'pt'
55
52
  end
56
53
 
57
- # Mapas de tradução entre PT e EN
58
54
  pt_para_en = GHERKIN_KEYS_PT.zip(GHERKIN_KEYS_EN).to_h
59
55
  en_para_pt = GHERKIN_KEYS_EN.zip(GHERKIN_KEYS_PT).to_h
60
56
 
61
- # Seleciona linhas que começam com palavras-chave Gherkin
57
+ # Seleciona apenas linhas que começam com palavras-chave Gherkin
62
58
  linhas_passos = linhas.map(&:strip).select do |linha|
63
59
  ALL_KEYS.any? { |chave| linha.start_with?(chave + ' ') }
64
60
  end
65
61
 
66
- # Se não encontrar passos, retorna false
67
62
  return false if linhas_passos.empty?
68
63
 
69
- # Cria diretório e arquivo de saída
70
64
  dir_saida = File.join(File.dirname(feature_path), 'steps')
71
65
  FileUtils.mkdir_p(dir_saida)
72
66
  arquivo_saida = File.join(dir_saida, "#{File.basename(feature_path, '.feature')}_steps.rb")
73
67
 
74
- # Cabeçalho do arquivo gerado
75
68
  conteudo = +"# encoding: utf-8\n"
76
69
  conteudo << "# Definições de passos geradas automaticamente para #{File.basename(feature_path)}\n\n"
77
70
 
71
+ passos_unicos = Set.new
72
+
78
73
  linhas_passos.each do |linha|
79
74
  palavra_original, restante = linha.split(' ', 2)
80
75
 
81
- # Define palavra-chave no idioma de saída
76
+ # Tradução de palavras-chave se necessário
82
77
  chave = case lang
83
78
  when 'en' then pt_para_en[palavra_original] || palavra_original
84
- else en_para_pt[palavra_original] || palavra_original
79
+ else en_para_pt[palavra_original] || palavra_original
85
80
  end
86
81
 
87
82
  texto_bruto = restante.dup
@@ -89,6 +84,7 @@ module Bddgenx
89
84
  padrao = ''
90
85
  tokens = []
91
86
 
87
+ # Analisa e parametriza o conteúdo dos passos
92
88
  until scanner.eos?
93
89
  if scanner.check(/"<([^>]+)>"/)
94
90
  scanner.scan(/"<([^>]+)>"/)
@@ -111,11 +107,14 @@ module Bddgenx
111
107
  end
112
108
  end
113
109
 
114
- # Escapa aspas no padrão
115
110
  padrao_seguro = padrao.gsub('"', '\\"')
116
- assinatura = "#{chave}(\"#{padrao_seguro}\")"
117
111
 
118
- # Adiciona parâmetros se existirem tokens
112
+ # Impede criação de métodos duplicados
113
+ next if passos_unicos.include?(padrao_seguro)
114
+
115
+ passos_unicos << padrao_seguro
116
+
117
+ assinatura = "#{chave}(\"#{padrao_seguro}\")"
119
118
  if tokens.any?
120
119
  argumentos = tokens.each_index.map { |i| "arg#{i+1}" }.join(', ')
121
120
  assinatura << " do |#{argumentos}|"
@@ -128,7 +127,6 @@ module Bddgenx
128
127
  conteudo << "end\n\n"
129
128
  end
130
129
 
131
- # Escreve arquivo de saída
132
130
  File.write(arquivo_saida, conteudo)
133
131
  puts "✅ Steps gerados: #{arquivo_saida}"
134
132
  true
@@ -0,0 +1,147 @@
1
+ # lib/bddgenx/ia/chatgpt_cliente.rb
2
+
3
+ module Bddgenx
4
+ module IA
5
+ ##
6
+ # Cliente para interação com a API do ChatGPT da OpenAI para gerar
7
+ # cenários BDD no formato Gherkin, com suporte a fallback para Gemini.
8
+ #
9
+ class ChatGptCliente
10
+ CHATGPT_API_URL = 'https://api.openai.com/v1/chat/completions'.freeze
11
+ MODEL = 'gpt-4o'
12
+
13
+ ##
14
+ # Gera cenários BDD a partir de uma história fornecida,
15
+ # solicitando à API do ChatGPT a criação dos cenários em formato Gherkin.
16
+ # Se a API key não estiver configurada ou houver erro na requisição,
17
+ # utiliza fallback com o GeminiCliente.
18
+ #
19
+ # @param historia [String] Texto com a história para basear os cenários.
20
+ # @param idioma [String] Código do idioma ('pt' ou 'en'), padrão 'pt'.
21
+ # @return [String] Cenários gerados em formato Gherkin com palavras-chave no idioma indicado.
22
+ #
23
+ def self.gerar_cenarios(historia, idioma = 'pt')
24
+ api_key = ENV['OPENAI_API_KEY']
25
+
26
+ unless api_key
27
+ warn "❌ API Key do ChatGPT não encontrada no .env (OPENAI_API_KEY)"
28
+ return fallback_com_gemini(historia, idioma)
29
+ end
30
+
31
+ # Palavras-chave Gherkin para português e inglês
32
+ keywords_pt = {
33
+ feature: "Funcionalidade",
34
+ scenario: "Cenário",
35
+ scenario_outline: "Esquema do Cenário",
36
+ examples: "Exemplos",
37
+ given: "Dado",
38
+ when: "Quando",
39
+ then: "Então",
40
+ and: "E"
41
+ }
42
+
43
+ keywords_en = {
44
+ feature: "Feature",
45
+ scenario: "Scenario",
46
+ scenario_outline: "Scenario Outline",
47
+ examples: "Examples",
48
+ given: "Given",
49
+ when: "When",
50
+ then: "Then",
51
+ and: "And"
52
+ }
53
+
54
+ keywords = idioma == 'en' ? keywords_en : keywords_pt
55
+
56
+ # Prompt base enviado ao ChatGPT, instruindo a saída no formato correto
57
+ prompt_base = <<~PROMPT
58
+ Gere cenários BDD no formato Gherkin, usando as palavras-chave de estrutura no idioma \"#{idioma}\":
59
+ Feature: #{keywords[:feature]}
60
+ Scenario: #{keywords[:scenario]}
61
+ Scenario Outline: #{keywords[:scenario_outline]}
62
+ Examples: #{keywords[:examples]}
63
+ Given: #{keywords[:given]}
64
+ When: #{keywords[:when]}
65
+ Then: #{keywords[:then]}
66
+ And: #{keywords[:and]}
67
+
68
+ Atenção: Os textos e descrições dos cenários e passos devem ser escritos em português, mesmo que as palavras-chave estejam em inglês.
69
+
70
+ História:
71
+ #{historia}
72
+ PROMPT
73
+
74
+ uri = URI(CHATGPT_API_URL)
75
+ request_body = {
76
+ model: MODEL,
77
+ messages: [
78
+ {
79
+ role: "user",
80
+ content: prompt_base
81
+ }
82
+ ]
83
+ }
84
+
85
+ headers = {
86
+ "Content-Type" => "application/json",
87
+ "Authorization" => "Bearer #{api_key}"
88
+ }
89
+
90
+ response = Net::HTTP.post(uri, request_body.to_json, headers)
91
+
92
+ if response.is_a?(Net::HTTPSuccess)
93
+ json = JSON.parse(response.body)
94
+ texto_ia = json.dig("choices", 0, "message", "content")
95
+
96
+ if texto_ia
97
+ texto_limpo = Bddgenx::GherkinCleaner.limpar(texto_ia)
98
+ Utils::StepCleaner.remover_steps_duplicados(texto_ia, idioma)
99
+
100
+ # Ajusta a linha de idioma no arquivo gerado
101
+ texto_limpo.sub!(/^# language: .*/, "# language: #{idioma}")
102
+ texto_limpo.prepend("# language: #{idioma}\n") unless texto_limpo.start_with?("# language:")
103
+ return texto_limpo
104
+ else
105
+ warn "❌ Resposta da IA sem conteúdo de texto"
106
+ warn JSON.pretty_generate(json)
107
+ return fallback_com_gemini(historia, idioma)
108
+ end
109
+ else
110
+ warn "❌ Erro ao chamar ChatGPT: #{response.code} - #{response.body}"
111
+ return fallback_com_gemini(historia, idioma)
112
+ end
113
+ end
114
+
115
+ ##
116
+ # Método de fallback que chama o GeminiCliente para gerar cenários,
117
+ # usado quando a API do ChatGPT não está disponível ou ocorre erro.
118
+ #
119
+ # @param historia [String] Texto da história para basear os cenários.
120
+ # @param idioma [String] Código do idioma ('pt' ou 'en').
121
+ # @return [String] Cenários gerados pelo GeminiCliente.
122
+ #
123
+ def self.fallback_com_gemini(historia, idioma)
124
+ warn "🔁 Tentando gerar com Gemini como fallback..."
125
+ GeminiCliente.gerar_cenarios(historia, idioma)
126
+ end
127
+
128
+ ##
129
+ # Detecta o idioma de um arquivo de feature pela linha "# language:".
130
+ #
131
+ # @param caminho_arquivo [String] Caminho para o arquivo de feature.
132
+ # @return [String] Código do idioma detectado ('pt' por padrão).
133
+ #
134
+ def self.detecta_idioma_arquivo(caminho_arquivo)
135
+ return 'pt' unless File.exist?(caminho_arquivo)
136
+
137
+ File.foreach(caminho_arquivo) do |linha|
138
+ if linha =~ /^#\s*language:\s*(\w{2})/i
139
+ return $1.downcase
140
+ end
141
+ end
142
+
143
+ 'pt'
144
+ end
145
+ end
146
+ end
147
+ end