airev 1.1.1__tar.gz → 1.3.0__tar.gz

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 (44) hide show
  1. {airev-1.1.1 → airev-1.3.0}/PKG-INFO +1 -1
  2. {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/PKG-INFO +1 -1
  3. {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/SOURCES.txt +1 -0
  4. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/__init__.py +1 -1
  5. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/cli.py +19 -7
  6. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/formatters/terminal.py +25 -2
  7. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/locales/en.yaml +7 -0
  8. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/locales/pt-br.yaml +7 -0
  9. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/models.py +2 -1
  10. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/prompt_builder.py +48 -0
  11. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/prompts/review_system.md +1 -1
  12. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/response_parser.py +5 -0
  13. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/updater/__init__.py +2 -1
  14. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/updater/http_client.py +1 -1
  15. airev-1.3.0/src/code_reviewer/updater/upgrade.py +182 -0
  16. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/updater/version_check.py +18 -0
  17. airev-1.3.0/tests/test_cli.py +29 -0
  18. {airev-1.1.1 → airev-1.3.0}/tests/test_prompt_builder.py +67 -0
  19. {airev-1.1.1 → airev-1.3.0}/tests/test_response_parser.py +7 -0
  20. {airev-1.1.1 → airev-1.3.0}/tests/test_updater.py +215 -8
  21. airev-1.1.1/src/code_reviewer/updater/upgrade.py +0 -87
  22. {airev-1.1.1 → airev-1.3.0}/README.md +0 -0
  23. {airev-1.1.1 → airev-1.3.0}/pyproject.toml +0 -0
  24. {airev-1.1.1 → airev-1.3.0}/setup.cfg +0 -0
  25. {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/dependency_links.txt +0 -0
  26. {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/entry_points.txt +0 -0
  27. {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/requires.txt +0 -0
  28. {airev-1.1.1 → airev-1.3.0}/src/airev.egg-info/top_level.txt +0 -0
  29. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/context_builder.py +0 -0
  30. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/diff_parser.py +0 -0
  31. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/formatters/__init__.py +0 -0
  32. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/formatters/progress.py +0 -0
  33. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/i18n/__init__.py +0 -0
  34. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/runners/__init__.py +0 -0
  35. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/runners/base.py +0 -0
  36. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/runners/copilot.py +0 -0
  37. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/runners/gemini.py +0 -0
  38. {airev-1.1.1 → airev-1.3.0}/src/code_reviewer/updater/notifier.py +0 -0
  39. {airev-1.1.1 → airev-1.3.0}/tests/test_context_builder.py +0 -0
  40. {airev-1.1.1 → airev-1.3.0}/tests/test_copilot_runner.py +0 -0
  41. {airev-1.1.1 → airev-1.3.0}/tests/test_diff_parser.py +0 -0
  42. {airev-1.1.1 → airev-1.3.0}/tests/test_i18n.py +0 -0
  43. {airev-1.1.1 → airev-1.3.0}/tests/test_progress.py +0 -0
  44. {airev-1.1.1 → airev-1.3.0}/tests/test_terminal_formatter.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: airev
3
- Version: 1.1.1
3
+ Version: 1.3.0
4
4
  Summary: CLI tool for AI-powered code review with context backtracking
5
5
  Author-email: Tarcisio <tarcisiojunior@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: airev
3
- Version: 1.1.1
3
+ Version: 1.3.0
4
4
  Summary: CLI tool for AI-powered code review with context backtracking
5
5
  Author-email: Tarcisio <tarcisiojunior@gmail.com>
6
6
  License: MIT
@@ -29,6 +29,7 @@ src/code_reviewer/updater/http_client.py
29
29
  src/code_reviewer/updater/notifier.py
30
30
  src/code_reviewer/updater/upgrade.py
31
31
  src/code_reviewer/updater/version_check.py
32
+ tests/test_cli.py
32
33
  tests/test_context_builder.py
33
34
  tests/test_copilot_runner.py
34
35
  tests/test_diff_parser.py
@@ -1,3 +1,3 @@
1
1
  """Code Reviewer - CLI para revisão de código com IA."""
2
2
 
3
- __version__ = "1.1.1"
3
+ __version__ = "1.3.0"
@@ -1,4 +1,4 @@
1
- """CLI Entry Point - Comando principal do code-reviewer."""
1
+ """CLI Entry Point - Comando principal do airev."""
2
2
 
3
3
  import sys
4
4
  import time
@@ -75,6 +75,13 @@ def main():
75
75
  type=click.Choice(get_available_languages()),
76
76
  help="Idioma das mensagens (default: pt-br)",
77
77
  )
78
+ @click.option(
79
+ "--text-quality",
80
+ "-t",
81
+ is_flag=True,
82
+ default=False,
83
+ help="Ativa verificação de ortografia e clareza em mensagens de usuário",
84
+ )
78
85
  def review(
79
86
  base: str,
80
87
  runner: str,
@@ -83,18 +90,21 @@ def review(
83
90
  no_progress: bool,
84
91
  progress: bool,
85
92
  lang: str,
93
+ text_quality: bool,
86
94
  ):
87
95
  """Analisa o diff da branch atual contra a branch base.
88
96
 
89
97
  Exemplos:
90
98
 
91
- code-reviewer review --base main
99
+ airev review --base main
100
+
101
+ airev review --base develop --runner copilot
92
102
 
93
- code-reviewer review --base develop --runner copilot
103
+ airev review --base main --json-output
94
104
 
95
- code-reviewer review --base main --json-output
105
+ airev review --base main --no-progress
96
106
 
97
- code-reviewer review --base main --no-progress
107
+ airev review --base main --text-quality
98
108
  """
99
109
  workdir = workdir or Path.cwd()
100
110
  start_time = time.perf_counter()
@@ -173,7 +183,9 @@ def review(
173
183
 
174
184
  # Monta o prompt
175
185
  with reporter.status(t("cli.building_prompt")):
176
- prompt = build_prompt(diff_files, context_graphs, current_branch, base)
186
+ prompt = build_prompt(
187
+ diff_files, context_graphs, current_branch, base, text_quality=text_quality
188
+ )
177
189
 
178
190
  # Obtém o runner
179
191
  try:
@@ -232,7 +244,7 @@ def runners():
232
244
 
233
245
  @main.command()
234
246
  def upgrade():
235
- """Atualiza o code-reviewer para a versão mais recente."""
247
+ """Atualiza o airev para a versão mais recente."""
236
248
  from rich.console import Console
237
249
 
238
250
  console = Console()
@@ -5,7 +5,7 @@ from typing import TextIO
5
5
  import sys
6
6
 
7
7
  from ..i18n import t
8
- from ..models import Finding, ReviewResult, Severity
8
+ from ..models import Category, Finding, ReviewResult, Severity
9
9
 
10
10
 
11
11
  # Códigos ANSI para cores
@@ -71,6 +71,28 @@ def format_severity(severity: Severity) -> str:
71
71
  return _colorize(f"[{severity.value}]", Colors.BOLD, Colors.BLUE)
72
72
 
73
73
 
74
+ def format_category_badge(category: Category) -> str:
75
+ """Formata o badge da categoria.
76
+
77
+ Args:
78
+ category: Categoria do finding
79
+
80
+ Returns:
81
+ String formatada com ícone e cor para a categoria
82
+ """
83
+ # Ícones e cores por categoria
84
+ category_styles = {
85
+ Category.SECURITY: ("🔒", Colors.RED),
86
+ Category.PERFORMANCE: ("⚡", Colors.YELLOW),
87
+ Category.BUG: ("🐛", Colors.MAGENTA),
88
+ Category.RESOURCE_LEAK: ("💧", Colors.CYAN),
89
+ Category.TEXT_QUALITY: ("✏️", Colors.CYAN),
90
+ }
91
+
92
+ icon, color = category_styles.get(category, ("•", Colors.WHITE))
93
+ return f"{icon} {_colorize(category.value, color)}"
94
+
95
+
74
96
  def format_finding(finding: Finding) -> str:
75
97
  """Formata um finding para exibição.
76
98
 
@@ -84,10 +106,11 @@ def format_finding(finding: Finding) -> str:
84
106
 
85
107
  # Header: [SEVERITY] arquivo:linha - Título
86
108
  severity_str = format_severity(finding.severity)
109
+ category_badge = format_category_badge(finding.category)
87
110
  location = _colorize(f"{finding.file}:{finding.line}", Colors.CYAN)
88
111
  title = _colorize(finding.title, Colors.BOLD)
89
112
 
90
- lines.append(f" {severity_str} {location} - {title}")
113
+ lines.append(f" {severity_str} {category_badge} {location} - {title}")
91
114
 
92
115
  # Descrição
93
116
  if finding.description:
@@ -47,6 +47,13 @@ terminal:
47
47
  # Findings
48
48
  suggestion: "Suggestion:"
49
49
 
50
+ # Categories
51
+ category_security: "security"
52
+ category_performance: "performance"
53
+ category_bug: "bug"
54
+ category_resource_leak: "resource leak"
55
+ category_text_quality: "text quality"
56
+
50
57
  # Summary
51
58
  summary: "SUMMARY:"
52
59
  findings_count: "{count} finding(s)"
@@ -47,6 +47,13 @@ terminal:
47
47
  # Findings
48
48
  suggestion: "Sugestão:"
49
49
 
50
+ # Categorias
51
+ category_security: "segurança"
52
+ category_performance: "performance"
53
+ category_bug: "bug"
54
+ category_resource_leak: "vazamento de recurso"
55
+ category_text_quality: "qualidade de texto"
56
+
50
57
  # Resumo
51
58
  summary: "RESUMO:"
52
59
  findings_count: "{count} finding(s)"
@@ -21,6 +21,7 @@ class Category(str, Enum):
21
21
  PERFORMANCE = "performance"
22
22
  BUG = "bug"
23
23
  RESOURCE_LEAK = "resource-leak"
24
+ TEXT_QUALITY = "text-quality"
24
25
 
25
26
 
26
27
  class DiffLine(BaseModel):
@@ -90,7 +91,7 @@ class Finding(BaseModel):
90
91
  line: int = Field(description="Número da linha")
91
92
  severity: Severity = Field(description="Severidade: CRITICAL, WARNING, INFO")
92
93
  category: Category = Field(
93
- description="Categoria: security, performance, bug, resource-leak"
94
+ description="Categoria: security, performance, bug, resource-leak, text-quality"
94
95
  )
95
96
  title: str = Field(description="Título curto do problema")
96
97
  description: str = Field(description="Descrição detalhada do problema")
@@ -152,11 +152,54 @@ def format_references_for_prompt(context_graphs: list[ContextGraph]) -> str:
152
152
  return "\n".join(parts)
153
153
 
154
154
 
155
+ def get_text_quality_section(language_name: str) -> str:
156
+ """Retorna a seção de instruções para verificação de qualidade de texto.
157
+
158
+ Args:
159
+ language_name: Nome do idioma para verificação
160
+
161
+ Returns:
162
+ String com instruções de verificação de texto
163
+ """
164
+ return f"""
165
+ ## QUALIDADE DE TEXTO
166
+
167
+ Verifique ortografia e clareza semântica em mensagens voltadas ao usuário, no idioma **{language_name}**.
168
+
169
+ ### O que verificar:
170
+
171
+ **Padrões de código:**
172
+ - `raise *Error("...")` e `raise *Exception("...")`
173
+ - `print("...")` e `console.log("...")`
174
+ - Parâmetros nomeados: `message=`, `label=`, `title=`, `description=`, `text=`
175
+ - Funções de UI: `flash("...")`, `toast("...")`, `alert("...")`
176
+
177
+ **Arquivos de i18n:**
178
+ - Arquivos em `locales/**/*`
179
+ - Arquivos em `i18n/**/*`
180
+ - Arquivos `messages.*` e `strings.*`
181
+
182
+ ### O que ignorar:
183
+
184
+ - Identificadores: snake_case, camelCase, PascalCase
185
+ - Termos técnicos: HTTP, JSON, API, SQL, URL, etc.
186
+ - Nomes próprios e termos de domínio específico
187
+ - Chaves de configuração e variáveis de ambiente
188
+
189
+ ### Formato dos findings:
190
+
191
+ - Categoria: `text-quality`
192
+ - Severidade: sempre `INFO`
193
+ - Inclua a correção sugerida no campo `suggestion`
194
+ """
195
+
196
+
155
197
  def build_prompt(
156
198
  diff_files: list[DiffFile],
157
199
  context_graphs: list[ContextGraph],
158
200
  branch: str,
159
201
  base: str,
202
+ text_quality: bool = False,
160
203
  ) -> str:
161
204
  """Monta o prompt completo para a IA.
162
205
 
@@ -165,6 +208,7 @@ def build_prompt(
165
208
  context_graphs: Grafos de contexto com backtracking
166
209
  branch: Nome da branch sendo analisada
167
210
  base: Nome da branch base
211
+ text_quality: Se True, inclui verificação de ortografia e clareza
168
212
 
169
213
  Returns:
170
214
  Prompt completo pronto para enviar à IA
@@ -183,11 +227,15 @@ def build_prompt(
183
227
  lang_code = get_language()
184
228
  language_name = LANGUAGE_NAMES.get(lang_code, lang_code)
185
229
 
230
+ # Seção de text-quality (condicional)
231
+ text_quality_section = get_text_quality_section(language_name) if text_quality else ""
232
+
186
233
  # Substitui placeholders
187
234
  prompt = template.replace("{diff}", diff_section)
188
235
  prompt = prompt.replace("{context}", context_section)
189
236
  prompt = prompt.replace("{references}", references_section)
190
237
  prompt = prompt.replace("{json_schema}", json_schema)
191
238
  prompt = prompt.replace("{language}", language_name)
239
+ prompt = prompt.replace("{text_quality_section}", text_quality_section)
192
240
 
193
241
  return prompt
@@ -25,7 +25,7 @@ O DIFF abaixo mostra APENAS as linhas alteradas, não o arquivo completo. Códig
25
25
  - **Performance**: N+1 queries, loops desnecessários, operações O(n²), falta de índices, carregamento excessivo
26
26
  - **Bugs potenciais**: null pointer, race conditions, off-by-one, divisão por zero, exceções não tratadas
27
27
  - **Recursos não fechados**: conexões de banco, arquivos, sockets, locks não liberados
28
-
28
+ {text_quality_section}
29
29
  ## FORMATO DE SAÍDA
30
30
 
31
31
  Retorne APENAS um JSON válido no formato abaixo. Não inclua explicações fora do JSON.
@@ -121,6 +121,11 @@ def normalize_category(value: str) -> Category:
121
121
  "resource_leak": Category.RESOURCE_LEAK,
122
122
  "leak": Category.RESOURCE_LEAK,
123
123
  "memory": Category.RESOURCE_LEAK,
124
+ "text-quality": Category.TEXT_QUALITY,
125
+ "text_quality": Category.TEXT_QUALITY,
126
+ "spelling": Category.TEXT_QUALITY,
127
+ "grammar": Category.TEXT_QUALITY,
128
+ "typo": Category.TEXT_QUALITY,
124
129
  }
125
130
 
126
131
  return mapping.get(value_lower, Category.BUG)
@@ -1,11 +1,12 @@
1
1
  """Módulo de auto-update para verificação e atualização de versões."""
2
2
 
3
- from .version_check import check_for_update, UpdateInfo
3
+ from .version_check import check_for_update, clear_cache, UpdateInfo
4
4
  from .notifier import notify_update
5
5
  from .upgrade import detect_installer, run_upgrade
6
6
 
7
7
  __all__ = [
8
8
  "check_for_update",
9
+ "clear_cache",
9
10
  "UpdateInfo",
10
11
  "notify_update",
11
12
  "detect_installer",
@@ -30,7 +30,7 @@ class UrllibClient:
30
30
  try:
31
31
  request = urllib.request.Request(
32
32
  url,
33
- headers={"Accept": "application/json", "User-Agent": "code-reviewer"},
33
+ headers={"Accept": "application/json", "User-Agent": "airev"},
34
34
  )
35
35
  with urllib.request.urlopen(request, timeout=timeout) as response:
36
36
  data = response.read().decode("utf-8")
@@ -0,0 +1,182 @@
1
+ """Funcionalidades de upgrade do pacote."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from rich.console import Console
9
+
10
+ from .. import __version__
11
+ from .version_check import check_for_update, clear_cache
12
+
13
+
14
+ def detect_installer() -> str:
15
+ """Detecta qual instalador foi usado (pipx ou pip).
16
+
17
+ Returns:
18
+ "pipx" se instalado via pipx, "pip" caso contrário
19
+ """
20
+ exe_path = Path(sys.executable)
21
+
22
+ # pipx instala em ~/.local/pipx/venvs/
23
+ if "pipx" in exe_path.parts:
24
+ return "pipx"
25
+
26
+ return "pip"
27
+
28
+
29
+ def get_installed_version(installer: str) -> str | None:
30
+ """Obtém a versão real instalada do airev consultando o instalador.
31
+
32
+ Args:
33
+ installer: "pipx" ou "pip"
34
+
35
+ Returns:
36
+ String com a versão instalada ou None se não conseguir obter
37
+ """
38
+ try:
39
+ if installer == "pipx":
40
+ # Tenta usar pipx list --json
41
+ result = subprocess.run(
42
+ ["pipx", "list", "--json"],
43
+ capture_output=True,
44
+ text=True,
45
+ check=False,
46
+ )
47
+ if result.returncode == 0:
48
+ data = json.loads(result.stdout)
49
+ venvs = data.get("venvs", {})
50
+ if "airev" in venvs:
51
+ metadata = venvs["airev"].get("metadata", {})
52
+ return metadata.get("main_package", {}).get("package_version")
53
+ # Fallback: tenta sem --json (versões antigas do pipx)
54
+ return _get_version_from_pipx_list_text()
55
+ else:
56
+ # pip show airev
57
+ result = subprocess.run(
58
+ [sys.executable, "-m", "pip", "show", "airev"],
59
+ capture_output=True,
60
+ text=True,
61
+ check=False,
62
+ )
63
+ if result.returncode == 0:
64
+ for line in result.stdout.splitlines():
65
+ if line.startswith("Version:"):
66
+ return line.split(":", 1)[1].strip()
67
+ except (subprocess.SubprocessError, json.JSONDecodeError, KeyError):
68
+ pass
69
+ return None
70
+
71
+
72
+ def _get_version_from_pipx_list_text() -> str | None:
73
+ """Fallback para obter versão do pipx list sem --json.
74
+
75
+ Returns:
76
+ String com a versão ou None se não encontrar
77
+ """
78
+ try:
79
+ result = subprocess.run(
80
+ ["pipx", "list"],
81
+ capture_output=True,
82
+ text=True,
83
+ check=False,
84
+ )
85
+ if result.returncode == 0:
86
+ # Procura por linha como "airev 1.0.0" ou "package airev 1.0.0"
87
+ for line in result.stdout.splitlines():
88
+ if "airev" in line.lower():
89
+ parts = line.split()
90
+ for i, part in enumerate(parts):
91
+ if "airev" in part.lower() and i + 1 < len(parts):
92
+ version = parts[i + 1].strip(",")
93
+ # Verifica se parece uma versão
94
+ if version and version[0].isdigit():
95
+ return version
96
+ except subprocess.SubprocessError:
97
+ pass
98
+ return None
99
+
100
+
101
+ def run_upgrade(console: Console | None = None) -> bool:
102
+ """Executa upgrade do pacote.
103
+
104
+ Args:
105
+ console: Console Rich para output (opcional)
106
+
107
+ Returns:
108
+ True se upgrade foi executado com sucesso, False caso contrário
109
+ """
110
+ if console is None:
111
+ console = Console()
112
+
113
+ # Verifica se há atualização disponível
114
+ console.print("[dim]Verificando atualizações...[/]")
115
+ update_info = check_for_update()
116
+
117
+ if update_info is None:
118
+ # Limpa cache mesmo quando não há atualização (garante consistência)
119
+ clear_cache()
120
+ console.print(
121
+ f"[green]✓[/] Você já está na versão mais recente ([cyan]{__version__}[/])"
122
+ )
123
+ return False
124
+
125
+ installer = detect_installer()
126
+
127
+ # Obtém versão antes do upgrade para comparação
128
+ version_before = get_installed_version(installer) or __version__
129
+
130
+ console.print(
131
+ f"[yellow]→[/] Atualizando de {version_before} "
132
+ f"para {update_info.latest_version}..."
133
+ )
134
+
135
+ # Monta comando de upgrade
136
+ if installer == "pipx":
137
+ cmd = ["pipx", "upgrade", "airev"]
138
+ else:
139
+ cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "airev"]
140
+
141
+ try:
142
+ result = subprocess.run(
143
+ cmd,
144
+ capture_output=True,
145
+ text=True,
146
+ check=False,
147
+ )
148
+
149
+ # Sempre limpa o cache após tentativa de upgrade
150
+ clear_cache()
151
+
152
+ if result.returncode == 0:
153
+ # Verifica se a versão realmente mudou
154
+ version_after = get_installed_version(installer)
155
+
156
+ if version_after and version_after != version_before:
157
+ console.print(
158
+ f"[green]✓[/] Atualizado de {version_before} para {version_after}"
159
+ )
160
+ return True
161
+ else:
162
+ # Comando executou mas versão não mudou
163
+ console.print(
164
+ f"[green]✓[/] Você já está na versão mais recente "
165
+ f"([cyan]{version_after or version_before}[/])"
166
+ )
167
+ return False
168
+ else:
169
+ console.print(f"[red]✗[/] Falha ao atualizar: {result.stderr.strip()}")
170
+ console.print(f"[dim]Tente manualmente: {' '.join(cmd)}[/]")
171
+ return False
172
+
173
+ except FileNotFoundError:
174
+ clear_cache()
175
+ console.print(f"[red]✗[/] Comando '{cmd[0]}' não encontrado")
176
+ if installer == "pipx":
177
+ console.print("[dim]Tente: pip install --upgrade airev[/]")
178
+ return False
179
+ except Exception as e:
180
+ clear_cache()
181
+ console.print(f"[red]✗[/] Erro inesperado: {e}")
182
+ return False
@@ -110,6 +110,24 @@ def _write_cache(latest_version: str) -> None:
110
110
  pass
111
111
 
112
112
 
113
+ def clear_cache() -> bool:
114
+ """Remove o arquivo de cache de verificação de versão.
115
+
116
+ Deve ser chamada após upgrade para garantir que a próxima
117
+ verificação consulte o PyPI novamente.
118
+
119
+ Returns:
120
+ True se o cache foi removido ou não existia, False se falhou
121
+ """
122
+ try:
123
+ if CACHE_FILE.exists():
124
+ CACHE_FILE.unlink()
125
+ return True
126
+ except OSError:
127
+ # Falha ao remover cache - ignora silenciosamente
128
+ return False
129
+
130
+
113
131
  def get_latest_version(timeout: float = DEFAULT_TIMEOUT) -> str | None:
114
132
  """Consulta PyPI para obter a versão mais recente.
115
133
 
@@ -0,0 +1,29 @@
1
+ """Testes para o CLI."""
2
+
3
+ from click.testing import CliRunner
4
+
5
+ from code_reviewer.cli import review
6
+
7
+
8
+ class TestReviewCommand:
9
+ """Testes para o comando review."""
10
+
11
+ def test_flag_text_quality_reconhecida(self):
12
+ """Verifica que a flag --text-quality é aceita pelo CLI."""
13
+ runner = CliRunner()
14
+
15
+ # Invoca com --help para verificar que a flag existe
16
+ result = runner.invoke(review, ["--help"])
17
+
18
+ assert result.exit_code == 0
19
+ assert "--text-quality" in result.output
20
+ assert "ortografia" in result.output or "spelling" in result.output.lower()
21
+
22
+ def test_flag_text_quality_short_form(self):
23
+ """Verifica que a forma curta -t funciona."""
24
+ runner = CliRunner()
25
+
26
+ result = runner.invoke(review, ["--help"])
27
+
28
+ assert result.exit_code == 0
29
+ assert "-t" in result.output
@@ -188,3 +188,70 @@ class TestBuildPrompt:
188
188
  assert "REGRAS" in prompt
189
189
  assert "test.py" in prompt
190
190
  assert "review" in prompt # Schema JSON
191
+
192
+ def test_prompt_sem_text_quality_por_padrao(self):
193
+ """Verifica que seção text-quality não aparece quando flag desativada."""
194
+ diff_files = [
195
+ DiffFile(
196
+ path="test.py",
197
+ hunks=[
198
+ DiffHunk(
199
+ function_name="test",
200
+ start_line_old=1,
201
+ start_line_new=1,
202
+ added_lines=[],
203
+ removed_lines=[],
204
+ )
205
+ ],
206
+ )
207
+ ]
208
+ context_graphs = []
209
+
210
+ prompt = build_prompt(diff_files, context_graphs, "feature/test", "main")
211
+
212
+ # Não deve conter seção de text-quality
213
+ assert "QUALIDADE DE TEXTO" not in prompt
214
+ assert "text-quality" not in prompt
215
+
216
+ def test_prompt_com_text_quality_ativo(self):
217
+ """Verifica que seção text-quality aparece quando flag ativada."""
218
+ diff_files = [
219
+ DiffFile(
220
+ path="test.py",
221
+ hunks=[
222
+ DiffHunk(
223
+ function_name="test",
224
+ start_line_old=1,
225
+ start_line_new=1,
226
+ added_lines=[],
227
+ removed_lines=[],
228
+ )
229
+ ],
230
+ )
231
+ ]
232
+ context_graphs = []
233
+
234
+ prompt = build_prompt(
235
+ diff_files, context_graphs, "feature/test", "main", text_quality=True
236
+ )
237
+
238
+ # Deve conter seção de text-quality
239
+ assert "QUALIDADE DE TEXTO" in prompt
240
+ assert "text-quality" in prompt
241
+ assert "ortografia" in prompt
242
+ assert "raise *Error" in prompt
243
+ assert "locales/" in prompt
244
+
245
+ def test_prompt_text_quality_menciona_ignorar_identificadores(self):
246
+ """Verifica que instruções de exclusão estão presentes."""
247
+ diff_files = []
248
+ context_graphs = []
249
+
250
+ prompt = build_prompt(
251
+ diff_files, context_graphs, "feature/test", "main", text_quality=True
252
+ )
253
+
254
+ # Deve instruir a ignorar identificadores
255
+ assert "snake_case" in prompt
256
+ assert "camelCase" in prompt
257
+ assert "termos técnicos" in prompt or "Termos técnicos" in prompt
@@ -117,6 +117,13 @@ class TestNormalizeCategory:
117
117
  assert normalize_category("resource-leak") == Category.RESOURCE_LEAK
118
118
  assert normalize_category("memory") == Category.RESOURCE_LEAK
119
119
 
120
+ def test_text_quality(self):
121
+ assert normalize_category("text-quality") == Category.TEXT_QUALITY
122
+ assert normalize_category("text_quality") == Category.TEXT_QUALITY
123
+ assert normalize_category("spelling") == Category.TEXT_QUALITY
124
+ assert normalize_category("grammar") == Category.TEXT_QUALITY
125
+ assert normalize_category("typo") == Category.TEXT_QUALITY
126
+
120
127
  def test_desconhecido_para_bug(self):
121
128
  assert normalize_category("unknown") == Category.BUG
122
129
 
@@ -1,6 +1,7 @@
1
1
  """Testes para o módulo de auto-update."""
2
2
 
3
3
  import json
4
+ import subprocess
4
5
  import sys
5
6
  from datetime import datetime, timedelta, timezone
6
7
  from io import StringIO
@@ -17,11 +18,16 @@ from code_reviewer.updater.version_check import (
17
18
  _read_cache,
18
19
  _write_cache,
19
20
  check_for_update,
21
+ clear_cache,
20
22
  compare_versions,
21
23
  get_latest_version,
22
24
  )
23
25
  from code_reviewer.updater.notifier import notify_update
24
- from code_reviewer.updater.upgrade import detect_installer, run_upgrade
26
+ from code_reviewer.updater.upgrade import (
27
+ detect_installer,
28
+ get_installed_version,
29
+ run_upgrade,
30
+ )
25
31
 
26
32
 
27
33
  class TestHttpClient:
@@ -159,6 +165,48 @@ class TestCache:
159
165
  assert cache is None
160
166
 
161
167
 
168
+ class TestClearCache:
169
+ """Testes para clear_cache."""
170
+
171
+ def test_clear_existing_cache(self, tmp_path):
172
+ """Deve remover arquivo de cache existente."""
173
+ cache_file = tmp_path / "update-check.json"
174
+ cache_file.write_text('{"test": true}')
175
+
176
+ with patch(
177
+ "code_reviewer.updater.version_check.CACHE_FILE", cache_file
178
+ ):
179
+ result = clear_cache()
180
+
181
+ assert result is True
182
+ assert not cache_file.exists()
183
+
184
+ def test_clear_nonexistent_cache(self, tmp_path):
185
+ """Deve retornar True quando cache não existe."""
186
+ cache_file = tmp_path / "nonexistent.json"
187
+
188
+ with patch(
189
+ "code_reviewer.updater.version_check.CACHE_FILE", cache_file
190
+ ):
191
+ result = clear_cache()
192
+
193
+ assert result is True
194
+
195
+ def test_clear_cache_permission_error(self, tmp_path):
196
+ """Deve retornar False quando não consegue remover."""
197
+ cache_file = tmp_path / "update-check.json"
198
+ cache_file.write_text('{"test": true}')
199
+
200
+ with patch(
201
+ "code_reviewer.updater.version_check.CACHE_FILE", cache_file
202
+ ), patch(
203
+ "pathlib.Path.unlink", side_effect=OSError("Permission denied")
204
+ ):
205
+ result = clear_cache()
206
+
207
+ assert result is False
208
+
209
+
162
210
  class TestCheckForUpdate:
163
211
  """Testes para check_for_update."""
164
212
 
@@ -252,7 +300,7 @@ class TestDetectInstaller:
252
300
 
253
301
  def test_detect_pipx(self):
254
302
  """Deve detectar pipx quando no path do executável."""
255
- fake_path = Path("/home/user/.local/pipx/venvs/code-reviewer/bin/python")
303
+ fake_path = Path("/home/user/.local/pipx/venvs/airev/bin/python")
256
304
  with patch.object(sys, "executable", str(fake_path)):
257
305
  result = detect_installer()
258
306
  assert result == "pipx"
@@ -272,59 +320,167 @@ class TestDetectInstaller:
272
320
  assert result == "pip"
273
321
 
274
322
 
323
+ class TestGetInstalledVersion:
324
+ """Testes para get_installed_version."""
325
+
326
+ def test_get_version_pipx_json(self):
327
+ """Deve obter versão do pipx list --json."""
328
+ pipx_json = {
329
+ "venvs": {
330
+ "airev": {
331
+ "metadata": {
332
+ "main_package": {
333
+ "package_version": "1.2.3"
334
+ }
335
+ }
336
+ }
337
+ }
338
+ }
339
+ mock_result = MagicMock(returncode=0, stdout=json.dumps(pipx_json))
340
+
341
+ with patch("subprocess.run", return_value=mock_result):
342
+ result = get_installed_version("pipx")
343
+
344
+ assert result == "1.2.3"
345
+
346
+ def test_get_version_pipx_not_installed(self):
347
+ """Deve retornar None quando airev não está instalado no pipx."""
348
+ pipx_json = {"venvs": {}}
349
+ mock_result = MagicMock(returncode=0, stdout=json.dumps(pipx_json))
350
+
351
+ with patch("subprocess.run", return_value=mock_result):
352
+ result = get_installed_version("pipx")
353
+
354
+ assert result is None
355
+
356
+ def test_get_version_pip(self):
357
+ """Deve obter versão do pip show."""
358
+ pip_output = """Name: airev
359
+ Version: 1.0.0
360
+ Summary: Code reviewer CLI
361
+ """
362
+ mock_result = MagicMock(returncode=0, stdout=pip_output)
363
+
364
+ with patch("subprocess.run", return_value=mock_result):
365
+ result = get_installed_version("pip")
366
+
367
+ assert result == "1.0.0"
368
+
369
+ def test_get_version_pip_not_installed(self):
370
+ """Deve retornar None quando pip show falha."""
371
+ mock_result = MagicMock(returncode=1, stdout="")
372
+
373
+ with patch("subprocess.run", return_value=mock_result):
374
+ result = get_installed_version("pip")
375
+
376
+ assert result is None
377
+
378
+ def test_get_version_subprocess_error(self):
379
+ """Deve retornar None em erro de subprocess."""
380
+ with patch("subprocess.run", side_effect=subprocess.SubprocessError):
381
+ result = get_installed_version("pipx")
382
+
383
+ assert result is None
384
+
385
+
275
386
  class TestRunUpgrade:
276
387
  """Testes para run_upgrade."""
277
388
 
278
389
  def test_upgrade_no_update_available(self):
279
- """Deve retornar False quando não há update."""
390
+ """Deve retornar False e limpar cache quando não há update."""
280
391
  from rich.console import Console
281
392
 
282
393
  console = Console(file=StringIO(), force_terminal=True)
283
394
 
284
395
  with patch(
285
396
  "code_reviewer.updater.upgrade.check_for_update", return_value=None
286
- ):
397
+ ), patch(
398
+ "code_reviewer.updater.upgrade.clear_cache", return_value=True
399
+ ) as mock_clear:
287
400
  result = run_upgrade(console)
288
401
 
289
402
  assert result is False
403
+ mock_clear.assert_called_once()
290
404
 
291
- def test_upgrade_success_pipx(self):
292
- """Deve executar pipx upgrade com sucesso."""
405
+ def test_upgrade_success_pipx_version_changed(self):
406
+ """Deve executar pipx upgrade e verificar mudança de versão."""
293
407
  from rich.console import Console
294
408
 
295
409
  console = Console(file=StringIO(), force_terminal=True)
296
410
  update_info = UpdateInfo(current_version="0.1.0", latest_version="0.2.0")
297
411
  mock_result = MagicMock(returncode=0, stderr="")
298
412
 
413
+ # Simula versão mudando de 0.1.0 para 0.2.0 após upgrade
414
+ version_calls = iter(["0.1.0", "0.2.0"])
415
+
299
416
  with patch(
300
417
  "code_reviewer.updater.upgrade.check_for_update",
301
418
  return_value=update_info,
302
419
  ), patch(
303
420
  "code_reviewer.updater.upgrade.detect_installer", return_value="pipx"
304
421
  ), patch(
422
+ "code_reviewer.updater.upgrade.get_installed_version",
423
+ side_effect=lambda _: next(version_calls),
424
+ ), patch(
425
+ "code_reviewer.updater.upgrade.clear_cache", return_value=True
426
+ ) as mock_clear, patch(
305
427
  "subprocess.run", return_value=mock_result
306
428
  ) as mock_run:
307
429
  result = run_upgrade(console)
308
430
 
309
431
  assert result is True
310
432
  mock_run.assert_called_once()
433
+ mock_clear.assert_called_once()
311
434
  cmd = mock_run.call_args[0][0]
312
435
  assert "pipx" in cmd
313
436
  assert "upgrade" in cmd
314
437
 
315
- def test_upgrade_success_pip(self):
316
- """Deve executar pip install --upgrade com sucesso."""
438
+ def test_upgrade_success_but_version_unchanged(self):
439
+ """Deve retornar False quando versão não muda após upgrade."""
440
+ from rich.console import Console
441
+
442
+ console = Console(file=StringIO(), force_terminal=True)
443
+ update_info = UpdateInfo(current_version="0.1.0", latest_version="0.2.0")
444
+ mock_result = MagicMock(returncode=0, stderr="")
445
+
446
+ with patch(
447
+ "code_reviewer.updater.upgrade.check_for_update",
448
+ return_value=update_info,
449
+ ), patch(
450
+ "code_reviewer.updater.upgrade.detect_installer", return_value="pipx"
451
+ ), patch(
452
+ "code_reviewer.updater.upgrade.get_installed_version",
453
+ return_value="0.1.0", # Versão não muda
454
+ ), patch(
455
+ "code_reviewer.updater.upgrade.clear_cache", return_value=True
456
+ ) as mock_clear, patch(
457
+ "subprocess.run", return_value=mock_result
458
+ ):
459
+ result = run_upgrade(console)
460
+
461
+ assert result is False
462
+ mock_clear.assert_called_once()
463
+
464
+ def test_upgrade_success_pip_version_changed(self):
465
+ """Deve executar pip install --upgrade e verificar mudança."""
317
466
  from rich.console import Console
318
467
 
319
468
  console = Console(file=StringIO(), force_terminal=True)
320
469
  update_info = UpdateInfo(current_version="0.1.0", latest_version="0.2.0")
321
470
  mock_result = MagicMock(returncode=0, stderr="")
322
471
 
472
+ version_calls = iter(["0.1.0", "0.2.0"])
473
+
323
474
  with patch(
324
475
  "code_reviewer.updater.upgrade.check_for_update",
325
476
  return_value=update_info,
326
477
  ), patch(
327
478
  "code_reviewer.updater.upgrade.detect_installer", return_value="pip"
479
+ ), patch(
480
+ "code_reviewer.updater.upgrade.get_installed_version",
481
+ side_effect=lambda _: next(version_calls),
482
+ ), patch(
483
+ "code_reviewer.updater.upgrade.clear_cache", return_value=True
328
484
  ), patch(
329
485
  "subprocess.run", return_value=mock_result
330
486
  ) as mock_run:
@@ -335,3 +491,54 @@ class TestRunUpgrade:
335
491
  cmd = mock_run.call_args[0][0]
336
492
  assert "--upgrade" in cmd
337
493
  assert "airev" in cmd
494
+
495
+ def test_upgrade_clears_cache_on_failure(self):
496
+ """Deve limpar cache mesmo quando upgrade falha."""
497
+ from rich.console import Console
498
+
499
+ console = Console(file=StringIO(), force_terminal=True)
500
+ update_info = UpdateInfo(current_version="0.1.0", latest_version="0.2.0")
501
+ mock_result = MagicMock(returncode=1, stderr="Error")
502
+
503
+ with patch(
504
+ "code_reviewer.updater.upgrade.check_for_update",
505
+ return_value=update_info,
506
+ ), patch(
507
+ "code_reviewer.updater.upgrade.detect_installer", return_value="pipx"
508
+ ), patch(
509
+ "code_reviewer.updater.upgrade.get_installed_version",
510
+ return_value="0.1.0",
511
+ ), patch(
512
+ "code_reviewer.updater.upgrade.clear_cache", return_value=True
513
+ ) as mock_clear, patch(
514
+ "subprocess.run", return_value=mock_result
515
+ ):
516
+ result = run_upgrade(console)
517
+
518
+ assert result is False
519
+ mock_clear.assert_called_once()
520
+
521
+ def test_upgrade_clears_cache_on_exception(self):
522
+ """Deve limpar cache quando ocorre exceção."""
523
+ from rich.console import Console
524
+
525
+ console = Console(file=StringIO(), force_terminal=True)
526
+ update_info = UpdateInfo(current_version="0.1.0", latest_version="0.2.0")
527
+
528
+ with patch(
529
+ "code_reviewer.updater.upgrade.check_for_update",
530
+ return_value=update_info,
531
+ ), patch(
532
+ "code_reviewer.updater.upgrade.detect_installer", return_value="pipx"
533
+ ), patch(
534
+ "code_reviewer.updater.upgrade.get_installed_version",
535
+ return_value="0.1.0",
536
+ ), patch(
537
+ "code_reviewer.updater.upgrade.clear_cache", return_value=True
538
+ ) as mock_clear, patch(
539
+ "subprocess.run", side_effect=FileNotFoundError
540
+ ):
541
+ result = run_upgrade(console)
542
+
543
+ assert result is False
544
+ mock_clear.assert_called_once()
@@ -1,87 +0,0 @@
1
- """Funcionalidades de upgrade do pacote."""
2
-
3
- import subprocess
4
- import sys
5
- from pathlib import Path
6
-
7
- from rich.console import Console
8
-
9
- from .. import __version__
10
- from .version_check import check_for_update
11
-
12
-
13
- def detect_installer() -> str:
14
- """Detecta qual instalador foi usado (pipx ou pip).
15
-
16
- Returns:
17
- "pipx" se instalado via pipx, "pip" caso contrário
18
- """
19
- exe_path = Path(sys.executable)
20
-
21
- # pipx instala em ~/.local/pipx/venvs/
22
- if "pipx" in exe_path.parts:
23
- return "pipx"
24
-
25
- return "pip"
26
-
27
-
28
- def run_upgrade(console: Console | None = None) -> bool:
29
- """Executa upgrade do pacote.
30
-
31
- Args:
32
- console: Console Rich para output (opcional)
33
-
34
- Returns:
35
- True se upgrade foi executado, False se já está atualizado ou falhou
36
- """
37
- if console is None:
38
- console = Console()
39
-
40
- # Verifica se há atualização disponível
41
- console.print("[dim]Verificando atualizações...[/]")
42
- update_info = check_for_update()
43
-
44
- if update_info is None:
45
- console.print(
46
- f"[green]✓[/] Você já está na versão mais recente ([cyan]{__version__}[/])"
47
- )
48
- return False
49
-
50
- installer = detect_installer()
51
- console.print(
52
- f"[yellow]→[/] Atualizando de {update_info.current_version} "
53
- f"para {update_info.latest_version}..."
54
- )
55
-
56
- # Monta comando de upgrade
57
- if installer == "pipx":
58
- cmd = ["pipx", "upgrade", "airev"]
59
- else:
60
- cmd = [sys.executable, "-m", "pip", "install", "--upgrade", "airev"]
61
-
62
- try:
63
- result = subprocess.run(
64
- cmd,
65
- capture_output=True,
66
- text=True,
67
- check=False,
68
- )
69
-
70
- if result.returncode == 0:
71
- console.print(
72
- f"[green]✓[/] Atualizado para versão {update_info.latest_version}"
73
- )
74
- return True
75
- else:
76
- console.print(f"[red]✗[/] Falha ao atualizar: {result.stderr.strip()}")
77
- console.print(f"[dim]Tente manualmente: {' '.join(cmd)}[/]")
78
- return False
79
-
80
- except FileNotFoundError:
81
- console.print(f"[red]✗[/] Comando '{cmd[0]}' não encontrado")
82
- if installer == "pipx":
83
- console.print("[dim]Tente: pip install --upgrade airev[/]")
84
- return False
85
- except Exception as e:
86
- console.print(f"[red]✗[/] Erro inesperado: {e}")
87
- return False
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes