code-review-ai-cli 1.3.4__tar.gz → 1.3.6__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.
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/PKG-INFO +7 -3
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/README.md +6 -2
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/PKG-INFO +7 -3
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/SOURCES.txt +2 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/pyproject.toml +1 -1
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/ai_review.py +0 -1
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/config.py +0 -2
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/git_utils.py +0 -1
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/llm_client.py +119 -223
- code_review_ai_cli-1.3.6/src/prompt_utils.py +56 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/prompts/config.yaml.template +0 -3
- code_review_ai_cli-1.3.6/src/prompts/review_prompt.md.template +91 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_config.py +8 -2
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_llm_client.py +163 -69
- code_review_ai_cli-1.3.6/tests/test_prompt_utils.py +129 -0
- code_review_ai_cli-1.3.4/src/prompts/review_prompt.md.template +0 -2
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/dependency_links.txt +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/entry_points.txt +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/requires.txt +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/top_level.txt +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/setup.cfg +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/__init__.py +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/formatter.py +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/src/tfs_client.py +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_ai_review.py +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_formatter.py +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_git_utils.py +0 -0
- {code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/tests/test_tfs_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-review-ai-cli
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.6
|
|
4
4
|
Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: code-review,ai,azure-devops,tfs,pull-request,llm
|
|
@@ -126,7 +126,6 @@ tfs:
|
|
|
126
126
|
# ca_bundle: C:/certs/corporate-root-ca.pem
|
|
127
127
|
|
|
128
128
|
review:
|
|
129
|
-
language: pt
|
|
130
129
|
verbosity: detailed
|
|
131
130
|
scope: diff_only
|
|
132
131
|
custom_prompt_file: review_prompt.md
|
|
@@ -205,7 +204,12 @@ Edit it to tailor the review to your team:
|
|
|
205
204
|
- Include business/architecture context
|
|
206
205
|
- Add examples of good/bad comments
|
|
207
206
|
|
|
208
|
-
|
|
207
|
+
**Rules can be scoped** to specific file types using language tags. During a review, the AI detects the files changed in the diff, identifies their extensions, and only loads:
|
|
208
|
+
|
|
209
|
+
- Rules marked with `<!-- lang: all -->` for all files.
|
|
210
|
+
- Rules matching the extensions of the files being reviewed. Example `<!-- lang: cs,ts -->` applied when .cs or .ts files are present.
|
|
211
|
+
|
|
212
|
+
The path for the **markdown-customizable prompt** is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
|
|
209
213
|
|
|
210
214
|
```yaml
|
|
211
215
|
review:
|
|
@@ -89,7 +89,6 @@ tfs:
|
|
|
89
89
|
# ca_bundle: C:/certs/corporate-root-ca.pem
|
|
90
90
|
|
|
91
91
|
review:
|
|
92
|
-
language: pt
|
|
93
92
|
verbosity: detailed
|
|
94
93
|
scope: diff_only
|
|
95
94
|
custom_prompt_file: review_prompt.md
|
|
@@ -168,7 +167,12 @@ Edit it to tailor the review to your team:
|
|
|
168
167
|
- Include business/architecture context
|
|
169
168
|
- Add examples of good/bad comments
|
|
170
169
|
|
|
171
|
-
|
|
170
|
+
**Rules can be scoped** to specific file types using language tags. During a review, the AI detects the files changed in the diff, identifies their extensions, and only loads:
|
|
171
|
+
|
|
172
|
+
- Rules marked with `<!-- lang: all -->` for all files.
|
|
173
|
+
- Rules matching the extensions of the files being reviewed. Example `<!-- lang: cs,ts -->` applied when .cs or .ts files are present.
|
|
174
|
+
|
|
175
|
+
The path for the **markdown-customizable prompt** is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
|
|
172
176
|
|
|
173
177
|
```yaml
|
|
174
178
|
review:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: code-review-ai-cli
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.6
|
|
4
4
|
Summary: Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: code-review,ai,azure-devops,tfs,pull-request,llm
|
|
@@ -126,7 +126,6 @@ tfs:
|
|
|
126
126
|
# ca_bundle: C:/certs/corporate-root-ca.pem
|
|
127
127
|
|
|
128
128
|
review:
|
|
129
|
-
language: pt
|
|
130
129
|
verbosity: detailed
|
|
131
130
|
scope: diff_only
|
|
132
131
|
custom_prompt_file: review_prompt.md
|
|
@@ -205,7 +204,12 @@ Edit it to tailor the review to your team:
|
|
|
205
204
|
- Include business/architecture context
|
|
206
205
|
- Add examples of good/bad comments
|
|
207
206
|
|
|
208
|
-
|
|
207
|
+
**Rules can be scoped** to specific file types using language tags. During a review, the AI detects the files changed in the diff, identifies their extensions, and only loads:
|
|
208
|
+
|
|
209
|
+
- Rules marked with `<!-- lang: all -->` for all files.
|
|
210
|
+
- Rules matching the extensions of the files being reviewed. Example `<!-- lang: cs,ts -->` applied when .cs or .ts files are present.
|
|
211
|
+
|
|
212
|
+
The path for the **markdown-customizable prompt** is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
|
|
209
213
|
|
|
210
214
|
```yaml
|
|
211
215
|
review:
|
{code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/SOURCES.txt
RENAMED
|
@@ -12,6 +12,7 @@ src/config.py
|
|
|
12
12
|
src/formatter.py
|
|
13
13
|
src/git_utils.py
|
|
14
14
|
src/llm_client.py
|
|
15
|
+
src/prompt_utils.py
|
|
15
16
|
src/tfs_client.py
|
|
16
17
|
src/prompts/config.yaml.template
|
|
17
18
|
src/prompts/review_prompt.md.template
|
|
@@ -20,4 +21,5 @@ tests/test_config.py
|
|
|
20
21
|
tests/test_formatter.py
|
|
21
22
|
tests/test_git_utils.py
|
|
22
23
|
tests/test_llm_client.py
|
|
24
|
+
tests/test_prompt_utils.py
|
|
23
25
|
tests/test_tfs_client.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-review-ai-cli"
|
|
7
|
-
version = "1.3.
|
|
7
|
+
version = "1.3.6"
|
|
8
8
|
description = "Automated AI-powered code review CLI for Azure DevOps / TFS Pull Requests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -858,7 +858,6 @@ def _show_config(config: ReviewConfig) -> None:
|
|
|
858
858
|
print(f" {c.CYAN}API Key:{c.RESET} {'✅ Configured' if has_effective_key else '❌ Not configured'}")
|
|
859
859
|
print(f" {c.CYAN}Temperature:{c.RESET} {config.temperature}")
|
|
860
860
|
print(f" {c.CYAN}Max Tokens:{c.RESET} {config.max_tokens}")
|
|
861
|
-
print(f" {c.CYAN}Language:{c.RESET} {config.review_language}")
|
|
862
861
|
print(f" {c.CYAN}Verbosity:{c.RESET} {config.verbosity}")
|
|
863
862
|
print(f" {c.CYAN}Format:{c.RESET} {config.output_format}")
|
|
864
863
|
print(f"\n {c.CYAN}TFS URL:{c.RESET} {config.tfs_base_url or '(not configured)'}")
|
|
@@ -85,7 +85,6 @@ class ReviewConfig:
|
|
|
85
85
|
tfs_repository: str = "" # Default repository (empty = all)
|
|
86
86
|
|
|
87
87
|
# --- Review -------------------------------------------------------
|
|
88
|
-
review_language: str = "pt" # Review language (pt/en)
|
|
89
88
|
verbosity: str = "detailed" # "quick" | "detailed" | "security"
|
|
90
89
|
review_scope: str = "diff_only" # "diff_only" | "full_code"
|
|
91
90
|
max_diff_files: int = 50 # Max diff files sent to LLM
|
|
@@ -211,7 +210,6 @@ class ReviewConfig:
|
|
|
211
210
|
"tfs_ca_bundle": ("tfs", "ca_bundle"),
|
|
212
211
|
"tfs_repository": ("tfs", "repository"),
|
|
213
212
|
# Review
|
|
214
|
-
"review_language": ("review", "language"),
|
|
215
213
|
"verbosity": ("review", "verbosity"),
|
|
216
214
|
"review_scope": ("review", "scope"),
|
|
217
215
|
"max_diff_files": ("review", "max_diff_files"),
|
|
@@ -17,6 +17,7 @@ import json
|
|
|
17
17
|
import os
|
|
18
18
|
|
|
19
19
|
from .config import ReviewConfig
|
|
20
|
+
from .prompt_utils import detect_langs, filter_prompt_by_langs
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class LLMError(Exception):
|
|
@@ -26,209 +27,124 @@ class LLMError(Exception):
|
|
|
26
27
|
# ---------------------------------------------------------------------------
|
|
27
28
|
# System Prompts
|
|
28
29
|
# ---------------------------------------------------------------------------
|
|
30
|
+
# Maps each verbosity level ("quick", "detailed", "security") to its
|
|
31
|
+
# English-only system prompt.
|
|
32
|
+
#
|
|
33
|
+
# Type:
|
|
34
|
+
# dict[str, str] — flat mapping of verbosity -> prompt string. There is
|
|
35
|
+
# no per-language nesting; each value is a plain ``str``.
|
|
36
|
+
#
|
|
37
|
+
# See Also:
|
|
38
|
+
# get_system_prompt: Resolves a prompt for a given verbosity, with a
|
|
39
|
+
# fallback to "detailed" when the key is not recognised.
|
|
29
40
|
SYSTEM_PROMPTS = {
|
|
30
|
-
"quick":
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"detailed": {
|
|
49
|
-
"pt": (
|
|
50
|
-
"És um code reviewer senior experiente. Analisa o diff de código "
|
|
51
|
-
"fornecido e retorna apenas comentários inline.\n\n"
|
|
52
|
-
"Formato de output — para cada problema encontrado, escreve exatamente:\n"
|
|
53
|
-
"- Linha <número_linha>: <descrição do problema> \n\n"
|
|
54
|
-
"Regras:\n"
|
|
55
|
-
"- Reporta APENAS problemas específicos associados a uma linha ou bloco concreto de código alterado.\n"
|
|
56
|
-
"- NÃO produzas secções de sumário (ex: 'Bugs Potenciais', 'Visão Geral de Segurança').\n"
|
|
57
|
-
"- NÃO produzas parágrafos introdutórios ou de fecho.\n"
|
|
58
|
-
"- Se não encontrares problemas, escreve apenas: Nenhum problema encontrado.\n"
|
|
59
|
-
"- Sê específico, objetivo e conciso. Responde em português."
|
|
60
|
-
),
|
|
61
|
-
"en": (
|
|
62
|
-
"You are an experienced Senior Code Reviewer. Analyze the provided code and return only inline comments.\n\n"
|
|
63
|
-
"Output format — for each issue found, output exactly:\n"
|
|
64
|
-
"- Line <line_number>: <issue description>\n\n"
|
|
65
|
-
"Rules:\n"
|
|
66
|
-
"- Report ONLY specific issues tied to a concrete line or block of changed code.\n"
|
|
67
|
-
"- Do NOT produce summary sections (e.g. 'Potential Bugs', 'Security Overview').\n"
|
|
68
|
-
"- Do NOT produce introductory or closing paragraphs.\n"
|
|
69
|
-
"- If no issues are found, output only: No issues found.\n"
|
|
70
|
-
"- Be specific, actionable, and concise."
|
|
71
|
-
),
|
|
72
|
-
},
|
|
73
|
-
"security": {
|
|
74
|
-
"pt": (
|
|
75
|
-
"És um especialista em segurança de aplicações. Analisa o diff "
|
|
76
|
-
"de código fornecido com foco EXCLUSIVO em segurança.\n\n"
|
|
77
|
-
"Procura por:\n"
|
|
78
|
-
"- SQL Injection\n"
|
|
79
|
-
"- Cross-Site Scripting (XSS)\n"
|
|
80
|
-
"- Cross-Site Request Forgery (CSRF)\n"
|
|
81
|
-
"- Credenciais hardcoded ou secrets expostos\n"
|
|
82
|
-
"- Vulnerabilidades de autenticação/autorização\n"
|
|
83
|
-
"- Insecure deserialization\n"
|
|
84
|
-
"- Path traversal\n"
|
|
85
|
-
"- Command injection\n"
|
|
86
|
-
"- Dependências com vulnerabilidades conhecidas\n"
|
|
87
|
-
"- Logging de informação sensível\n"
|
|
88
|
-
"- Configurações inseguras\n\n"
|
|
89
|
-
"Fornece recomendações de correção para cada problema. "
|
|
90
|
-
"Responde em português."
|
|
91
|
-
),
|
|
92
|
-
"en": (
|
|
93
|
-
"You are an application security. Analyze the "
|
|
94
|
-
"provided code diff with EXCLUSIVE focus on security.\n\n"
|
|
95
|
-
"Look for:\n"
|
|
96
|
-
"- SQL Injection\n"
|
|
97
|
-
"- Cross-Site Scripting (XSS)\n"
|
|
98
|
-
"- Cross-Site Request Forgery (CSRF)\n"
|
|
99
|
-
"- Hardcoded credentials or exposed secrets\n"
|
|
100
|
-
"- Authentication/authorization vulnerabilities\n"
|
|
101
|
-
"- Insecure deserialization\n"
|
|
102
|
-
"- Path traversal\n"
|
|
103
|
-
"- Command injection\n"
|
|
104
|
-
"- Dependencies with known vulnerabilities\n"
|
|
105
|
-
"- Logging of sensitive information\n"
|
|
106
|
-
"- Insecure configurations\n\n"
|
|
107
|
-
"Provide fix recommendations for each issue."
|
|
108
|
-
),
|
|
109
|
-
},
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
# Special prompt for PR review with a combined narrative summary + structured comments
|
|
113
|
-
PR_COMMENT_PROMPT = {
|
|
114
|
-
"pt": (
|
|
115
|
-
"Analisa o diff de código de um Pull Request e retorna a tua resposta num único objeto JSON "
|
|
116
|
-
"com dois campos:\n"
|
|
117
|
-
'- "summary": um resumo narrativo do review, em texto (string). Se não encontrares problemas, '
|
|
118
|
-
'escreve algo como "Nenhum problema encontrado."\n'
|
|
119
|
-
'- "comments": um array de objetos, um por cada problema encontrado, com:\n'
|
|
120
|
-
' - "file": caminho do ficheiro (ex: "src/auth.py")\n'
|
|
121
|
-
' - "line": número da linha no diff (inteiro, ou 0 se geral)\n'
|
|
122
|
-
' - "type": tipo de issue ("bug", "security", "performance", "style", "suggestion", "praise")\n'
|
|
123
|
-
' - "comment": descrição direta do problema em português, sem saudações e sem emojis\n'
|
|
124
|
-
' - "suggestion": sugestão de correção (opcional, string vazia se não aplicável)\n'
|
|
125
|
-
' - "reference": fonte ou referência para o problema (URL de documentação, padrão ou princípio). '
|
|
126
|
-
'Importante: incluir SEMPRE uma referência relevante.\n\n'
|
|
127
|
-
"No campo 'comment', escreve de forma objetiva e curta. "
|
|
128
|
-
"Não uses introduções como 'Olá' ou 'Como code reviewer sénior'.\n"
|
|
129
|
-
"No campo 'reference', inclui uma fonte confiável, padrão ou link para documentação relevante.\n\n"
|
|
130
|
-
"Responde APENAS com um JSON object válido. Exemplo:\n"
|
|
131
|
-
'{\n'
|
|
132
|
-
' "summary": "Resumo narrativo curto do review...",\n'
|
|
133
|
-
' "comments": [\n'
|
|
134
|
-
' {\n'
|
|
135
|
-
' "file": "src/auth.py",\n'
|
|
136
|
-
' "line": 42,\n'
|
|
137
|
-
' "type": "security",\n'
|
|
138
|
-
' "comment": "Password armazenada em texto simples sem hashing",\n'
|
|
139
|
-
' "suggestion": "Usar bcrypt ou argon2 para hash de passwords",\n'
|
|
140
|
-
' "reference": "OWASP - Password Storage Cheat Sheet"\n'
|
|
141
|
-
' }\n'
|
|
142
|
-
' ]\n'
|
|
143
|
-
'}\n\n'
|
|
41
|
+
"quick": (
|
|
42
|
+
"You are an experienced Senior Code Reviewer. Analyze the provided code diff "
|
|
43
|
+
"and give a CONCISE review. Focus on critical issues:\n"
|
|
44
|
+
"- Bugs and logic errors\n"
|
|
45
|
+
"- Security issues\n"
|
|
46
|
+
"- Major performance problems\n\n"
|
|
47
|
+
"Format: Bullet points with file and line when possible. "
|
|
48
|
+
),
|
|
49
|
+
"detailed": (
|
|
50
|
+
"You are an experienced Senior Code Reviewer. Analyze the provided code and return only inline comments.\n\n"
|
|
51
|
+
"Output format — for each issue found, output exactly:\n"
|
|
52
|
+
"- Line <line_number>: <issue description>\n\n"
|
|
53
|
+
"Rules:\n"
|
|
54
|
+
"- Report ONLY specific issues tied to a concrete line or block of changed code.\n"
|
|
55
|
+
"- Do NOT produce summary sections (e.g. 'Potential Bugs', 'Security Overview').\n"
|
|
56
|
+
"- Do NOT produce introductory or closing paragraphs.\n"
|
|
57
|
+
"- If no issues are found, output only: No issues found.\n"
|
|
58
|
+
"- Be specific, actionable, and concise."
|
|
144
59
|
),
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
"
|
|
159
|
-
"
|
|
160
|
-
"
|
|
161
|
-
"Respond ONLY with a valid JSON object. Example:\n"
|
|
162
|
-
'{\n'
|
|
163
|
-
' "summary": "Short narrative summary of the review...",\n'
|
|
164
|
-
' "comments": [\n'
|
|
165
|
-
' {\n'
|
|
166
|
-
' "file": "src/auth.py",\n'
|
|
167
|
-
' "line": 42,\n'
|
|
168
|
-
' "type": "security",\n'
|
|
169
|
-
' "comment": "Password stored in plain text without hashing",\n'
|
|
170
|
-
' "suggestion": "Use bcrypt or argon2 to hash passwords",\n'
|
|
171
|
-
' "reference": "OWASP - Password Storage Cheat Sheet"\n'
|
|
172
|
-
' }\n'
|
|
173
|
-
' ]\n'
|
|
174
|
-
'}\n\n'
|
|
60
|
+
"security": (
|
|
61
|
+
"You are an application security. Analyze the "
|
|
62
|
+
"provided code diff with EXCLUSIVE focus on security.\n\n"
|
|
63
|
+
"Look for:\n"
|
|
64
|
+
"- SQL Injection\n"
|
|
65
|
+
"- Cross-Site Scripting (XSS)\n"
|
|
66
|
+
"- Cross-Site Request Forgery (CSRF)\n"
|
|
67
|
+
"- Hardcoded credentials or exposed secrets\n"
|
|
68
|
+
"- Authentication/authorization vulnerabilities\n"
|
|
69
|
+
"- Insecure deserialization\n"
|
|
70
|
+
"- Path traversal\n"
|
|
71
|
+
"- Command injection\n"
|
|
72
|
+
"- Dependencies with known vulnerabilities\n"
|
|
73
|
+
"- Logging of sensitive information\n"
|
|
74
|
+
"- Insecure configurations\n\n"
|
|
75
|
+
"Provide fix recommendations for each issue."
|
|
175
76
|
),
|
|
176
77
|
}
|
|
177
78
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
79
|
+
# Special prompt for PR review with a combined narrative summary + structured comments.
|
|
80
|
+
#
|
|
81
|
+
# Type:
|
|
82
|
+
# str — a single English-only prompt (not a per-language dict) instructing
|
|
83
|
+
# the LLM to respond with one JSON object containing a "summary" string
|
|
84
|
+
# and a "comments" array (each with file, line, type, comment,
|
|
85
|
+
# suggestion, and reference fields).
|
|
86
|
+
#
|
|
87
|
+
# See Also:
|
|
88
|
+
# LLMClient.review_pr: Combines this prompt with the verbosity system
|
|
89
|
+
# prompt (from get_system_prompt) and scope guidance (from
|
|
90
|
+
# get_scope_guidance) to build the final system prompt sent to the LLM.
|
|
91
|
+
PR_COMMENT_PROMPT = (
|
|
92
|
+
"Analyze the Pull Request code diff and return your response as a single JSON object with two "
|
|
93
|
+
"fields:\n"
|
|
94
|
+
'- "summary": a narrative text summary of the review (string). If no issues are found, write '
|
|
95
|
+
'something like "No issues found."\n'
|
|
96
|
+
'- "comments": an array of objects, one per issue found, with:\n'
|
|
97
|
+
' - "file": file path (e.g., "src/auth.py")\n'
|
|
98
|
+
' - "line": line number in diff (integer, or 0 if general)\n'
|
|
99
|
+
' - "type": issue type ("bug", "security", "performance", "style", "suggestion", "praise")\n'
|
|
100
|
+
' - "comment": direct description of the issue, with no greetings and no emojis\n'
|
|
101
|
+
' - "suggestion": fix suggestion (optional, empty string if not applicable)\n'
|
|
102
|
+
' - "reference": source or reference for the issue (e.g., "OWASP Top 10", "PEP 8", documentation '
|
|
103
|
+
'URL, standard or principle). Important: ALWAYS include a relevant reference.\n\n'
|
|
104
|
+
"In 'comment', use a short and objective tone. "
|
|
105
|
+
"Do not include intros like 'Hello' or 'As a senior reviewer'.\n"
|
|
106
|
+
"In 'reference', include a trusted source, standard or link to relevant documentation.\n\n"
|
|
107
|
+
"Respond ONLY with a valid JSON object. Example:\n"
|
|
108
|
+
'{\n'
|
|
109
|
+
' "summary": "Short narrative summary of the review...",\n'
|
|
110
|
+
' "comments": [\n'
|
|
111
|
+
' {\n'
|
|
112
|
+
' "file": "src/auth.py",\n'
|
|
113
|
+
' "line": 42,\n'
|
|
114
|
+
' "type": "security",\n'
|
|
115
|
+
' "comment": "Password stored in plain text without hashing",\n'
|
|
116
|
+
' "suggestion": "Use bcrypt or argon2 to hash passwords",\n'
|
|
117
|
+
' "reference": "OWASP - Password Storage Cheat Sheet"\n'
|
|
118
|
+
' }\n'
|
|
119
|
+
' ]\n'
|
|
120
|
+
'}\n\n'
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def get_system_prompt(verbosity: str) -> str:
|
|
125
|
+
"""Returns the system prompt for the given verbosity level.
|
|
181
126
|
|
|
182
127
|
Selects a prompt from ``SYSTEM_PROMPTS`` keyed by *verbosity*. If the
|
|
183
128
|
requested verbosity is not found, the function falls back to
|
|
184
|
-
``"detailed"``.
|
|
185
|
-
resolved by *language*; if the language key is absent, ``"pt"``
|
|
186
|
-
(Portuguese) is used as the default.
|
|
129
|
+
``"detailed"``.
|
|
187
130
|
|
|
188
131
|
The ``"detailed"`` prompt instructs the LLM to return **only** inline
|
|
189
132
|
comments in the format ``- Line <n>: <description>``,
|
|
190
133
|
prohibiting summary sections and introductory/closing paragraphs.
|
|
191
134
|
When no issues are found the expected output is ``No issues found.``
|
|
192
|
-
(English) or ``Nenhum problema encontrado.`` (Portuguese).
|
|
193
135
|
|
|
194
136
|
Args:
|
|
195
137
|
verbosity: Review depth key — one of ``"quick"``, ``"detailed"``,
|
|
196
138
|
or ``"security"``. Any unrecognised value falls back to
|
|
197
139
|
``"detailed"``.
|
|
198
|
-
language: Response language code — ``"en"`` for English,
|
|
199
|
-
``"pt"`` for Portuguese.
|
|
200
140
|
|
|
201
141
|
Returns:
|
|
202
|
-
The system prompt string for the resolved verbosity
|
|
142
|
+
The system prompt string for the resolved verbosity.
|
|
203
143
|
"""
|
|
204
|
-
|
|
205
|
-
return prompts.get(language, prompts["pt"])
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def get_pr_comment_prompt(language: str) -> str:
|
|
209
|
-
"""Returns the prompt that requests structured JSON PR comments from the LLM.
|
|
144
|
+
return SYSTEM_PROMPTS.get(verbosity, SYSTEM_PROMPTS["detailed"])
|
|
210
145
|
|
|
211
|
-
Retrieves the prompt from ``PR_COMMENT_PROMPT`` for the given *language*.
|
|
212
|
-
If the language key is not present, ``"pt"`` (Portuguese) is used as the
|
|
213
|
-
default.
|
|
214
146
|
|
|
215
|
-
|
|
216
|
-
object with a ``"summary"`` field (narrative review text) and a
|
|
217
|
-
``"comments"`` field (array of review comment objects, each with the
|
|
218
|
-
fields ``file``, ``line``, ``type``, ``comment``, ``suggestion``,
|
|
219
|
-
and ``reference``).
|
|
220
|
-
|
|
221
|
-
Args:
|
|
222
|
-
language: Response language code — ``"en"`` for English,
|
|
223
|
-
``"pt"`` for Portuguese.
|
|
224
|
-
|
|
225
|
-
Returns:
|
|
226
|
-
The prompt string used to request structured PR comments.
|
|
227
|
-
"""
|
|
228
|
-
return PR_COMMENT_PROMPT.get(language, PR_COMMENT_PROMPT["pt"])
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
def get_scope_guidance(review_scope: str, language: str, structured: bool = False) -> str:
|
|
147
|
+
def get_scope_guidance(review_scope: str, structured: bool = False) -> str:
|
|
232
148
|
"""Returns LLM instructions tailored to the active review scope.
|
|
233
149
|
|
|
234
150
|
For ``diff_only`` scope, the instructions inform the LLM that:
|
|
@@ -248,8 +164,6 @@ def get_scope_guidance(review_scope: str, language: str, structured: bool = Fals
|
|
|
248
164
|
|
|
249
165
|
Args:
|
|
250
166
|
review_scope: ``"diff_only"`` (default) or ``"full_code"``.
|
|
251
|
-
language: Response language code — ``"en"`` for English,
|
|
252
|
-
``"pt"`` for Portuguese.
|
|
253
167
|
structured: When ``True``, appends additional constraints for the
|
|
254
168
|
structured JSON comment mode (every comment must carry a valid
|
|
255
169
|
file path and line number > 0 for inline posting).
|
|
@@ -260,52 +174,29 @@ def get_scope_guidance(review_scope: str, language: str, structured: bool = Fals
|
|
|
260
174
|
scope = (review_scope or "diff_only").lower()
|
|
261
175
|
|
|
262
176
|
if scope == "full_code":
|
|
263
|
-
if language == "en":
|
|
264
|
-
return (
|
|
265
|
-
"Review scope: full_code. The diff contains only added lines (+) for each file. "
|
|
266
|
-
"Analyze the complete content of the changed files and identify issues in the new code. "
|
|
267
|
-
"Do not comment on deleted or absent code."
|
|
268
|
-
)
|
|
269
177
|
return (
|
|
270
|
-
"
|
|
271
|
-
"
|
|
272
|
-
"
|
|
178
|
+
"Review scope: full_code. The diff contains only added lines (+) for each file. "
|
|
179
|
+
"Analyze the complete content of the changed files and identify issues in the new code. "
|
|
180
|
+
"Do not comment on deleted or absent code."
|
|
273
181
|
)
|
|
274
182
|
|
|
275
183
|
if structured:
|
|
276
|
-
if language == "en":
|
|
277
|
-
return (
|
|
278
|
-
"Review scope: diff_only. The diff contains only added lines (+) — context and deletions were removed. "
|
|
279
|
-
"The complete new-version file content, after the changes (between ### FULL_FILE_CONTEXT_START and ### FULL_FILE_CONTEXT_END markers) "
|
|
280
|
-
"is provided for each file as read-only context. "
|
|
281
|
-
"Use it to understand the surrounding code, but focus your review EXCLUSIVELY on the changed lines (marked + in the diff). "
|
|
282
|
-
"Do NOT report issues in unchanged lines unless they directly affect the correctness of the changes. "
|
|
283
|
-
"For every problem, you MUST provide a valid file and line (>0) to allow inline comments. "
|
|
284
|
-
"Do not emit general problem comments without file/line."
|
|
285
|
-
)
|
|
286
|
-
return (
|
|
287
|
-
"Ambito de review: diff_only. O diff contém apenas linhas adicionadas (+) — contexto e eliminações foram removidos. "
|
|
288
|
-
"É fornecido o conteúdo completo do ficheiro na versão nova, depois das alterações (entre os marcadores ### FULL_FILE_CONTEXT_START e ### FULL_FILE_CONTEXT_END) como contexto de leitura. "
|
|
289
|
-
"Usa-a para compreender o código envolvente, mas foca o teu review EXCLUSIVAMENTE nas linhas alteradas (marcadas com + no diff). "
|
|
290
|
-
"NÃO reportes problemas em linhas não alteradas, exceto se afetarem diretamente a correção das alterações. "
|
|
291
|
-
"Para cada problema, DEVE ser fornecido file e line válidos (>0) para comentário inline. "
|
|
292
|
-
"Não emitas comentários gerais de problema sem file/line."
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
if language == "en":
|
|
296
184
|
return (
|
|
297
|
-
"Review scope: diff_only. The diff contains only added lines (+). "
|
|
298
|
-
"The new-version
|
|
185
|
+
"Review scope: diff_only. The diff contains only added lines (+) — context and deletions were removed. "
|
|
186
|
+
"The complete new-version file content, after the changes (between ### FULL_FILE_CONTEXT_START and ### FULL_FILE_CONTEXT_END markers) "
|
|
299
187
|
"is provided for each file as read-only context. "
|
|
300
|
-
"Use it to understand the surrounding code, but focus your review EXCLUSIVELY on the changed lines. "
|
|
301
|
-
"Do NOT report issues in unchanged lines unless they directly affect the correctness of the changes."
|
|
188
|
+
"Use it to understand the surrounding code, but focus your review EXCLUSIVELY on the changed lines (marked + in the diff). "
|
|
189
|
+
"Do NOT report issues in unchanged lines unless they directly affect the correctness of the changes. "
|
|
190
|
+
"For every problem, you MUST provide a valid file and line (>0) to allow inline comments. "
|
|
191
|
+
"Do not emit general problem comments without file/line."
|
|
302
192
|
)
|
|
193
|
+
|
|
303
194
|
return (
|
|
304
|
-
"
|
|
305
|
-
"
|
|
306
|
-
"
|
|
307
|
-
"
|
|
308
|
-
"
|
|
195
|
+
"Review scope: diff_only. The diff contains only added lines (+). "
|
|
196
|
+
"The new-version (post-change) full file content (between ### FULL_FILE_CONTEXT_START and ### FULL_FILE_CONTEXT_END markers) "
|
|
197
|
+
"is provided for each file as read-only context. "
|
|
198
|
+
"Use it to understand the surrounding code, but focus your review EXCLUSIVELY on the changed lines. "
|
|
199
|
+
"Do NOT report issues in unchanged lines unless they directly affect the correctness of the changes."
|
|
309
200
|
)
|
|
310
201
|
|
|
311
202
|
|
|
@@ -407,13 +298,18 @@ class LLMClient:
|
|
|
407
298
|
is a list of dicts with keys: file, line, type, comment, suggestion,
|
|
408
299
|
reference.
|
|
409
300
|
"""
|
|
410
|
-
base_prompt = get_system_prompt(self.config.verbosity
|
|
411
|
-
json_schema_prompt =
|
|
301
|
+
base_prompt = get_system_prompt(self.config.verbosity)
|
|
302
|
+
json_schema_prompt = PR_COMMENT_PROMPT
|
|
412
303
|
custom_prompt = self._load_custom_prompt_text()
|
|
413
304
|
|
|
305
|
+
# filter custom_prompt sections by the languages/extensions actually changed
|
|
306
|
+
if custom_prompt:
|
|
307
|
+
file_paths = [f.get("file") for f in files_summary]
|
|
308
|
+
active_langs = detect_langs(file_paths)
|
|
309
|
+
custom_prompt = filter_prompt_by_langs(custom_prompt, active_langs) # can return an empty list
|
|
310
|
+
|
|
414
311
|
scope_guidance = get_scope_guidance(
|
|
415
312
|
review_scope=review_scope,
|
|
416
|
-
language=self.config.review_language,
|
|
417
313
|
structured=True,
|
|
418
314
|
)
|
|
419
315
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prompt Utilities Module - AI Code Review
|
|
3
|
+
========================================
|
|
4
|
+
Responsible for tailoring the custom review prompt (review_prompt.md) to the
|
|
5
|
+
files actually changed in a diff:
|
|
6
|
+
- Detects which "languages" (file extensions) are present in the changeset
|
|
7
|
+
- Filters the custom prompt content, keeping only the sections tagged for
|
|
8
|
+
those extensions (plus sections tagged "all", which always apply)
|
|
9
|
+
|
|
10
|
+
No manual extension-to-language mapping is required: the file extension
|
|
11
|
+
itself (without the dot, lowercase) is used as the tag. This means the
|
|
12
|
+
prompt file can be extended with new languages (e.g. Java, Go) without any
|
|
13
|
+
code changes - just add a new <!-- lang: ... --> section to review_prompt.md.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import re
|
|
18
|
+
|
|
19
|
+
LANG_TAG_RE = re.compile(r"<!--\s*lang:\s*(.*?)\s*-->")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def detect_langs(file_paths: list[str]) -> set[str]:
|
|
23
|
+
"""
|
|
24
|
+
Detects active tags directly from the extensions of changed files.
|
|
25
|
+
'all' is always included since general sections always apply.
|
|
26
|
+
No manual mapping: the extension (no dot, lowercase) IS the tag.
|
|
27
|
+
"""
|
|
28
|
+
langs = {"all"}
|
|
29
|
+
for path in file_paths:
|
|
30
|
+
if not path:
|
|
31
|
+
continue
|
|
32
|
+
ext = os.path.splitext(path)[1].lstrip(".").lower()
|
|
33
|
+
if ext:
|
|
34
|
+
langs.add(ext)
|
|
35
|
+
return langs
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def filter_prompt_by_langs(md_content: str, active_langs: set[str]) -> str:
|
|
39
|
+
"""
|
|
40
|
+
Parses review_prompt.md and returns only the sections whose
|
|
41
|
+
<!-- lang: ... --> tag intersects with active_langs.
|
|
42
|
+
"""
|
|
43
|
+
active_langs = {l.strip().lower() for l in active_langs}
|
|
44
|
+
blocks = re.split(r"(<!--\s*lang:.*?-->)", md_content)
|
|
45
|
+
|
|
46
|
+
output: list[str] = []
|
|
47
|
+
current_langs = {"all"}
|
|
48
|
+
for chunk in blocks:
|
|
49
|
+
match = LANG_TAG_RE.match(chunk.strip())
|
|
50
|
+
if match:
|
|
51
|
+
current_langs = {l.strip().lower() for l in match.group(1).split(",")}
|
|
52
|
+
continue
|
|
53
|
+
if current_langs & active_langs:
|
|
54
|
+
output.append(chunk)
|
|
55
|
+
|
|
56
|
+
return "\n".join(output).strip()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<!-- lang: all -->
|
|
2
|
+
## General Rules (all languages)
|
|
3
|
+
- Security: SQL injection, XSS, hardcoded secrets/credentials, missing input validation, sensitive data exposed in logs.
|
|
4
|
+
- Error handling: swallowed exceptions (empty catch), missing logging at critical points, generic error messages to end users.
|
|
5
|
+
- Readability: methods too long or with too many responsibilities (SRP), poor naming, dead/commented-out code.
|
|
6
|
+
- Duplication: repeated logic that should be extracted/reused.
|
|
7
|
+
- Testability: hard-to-mock dependencies, business logic mixed with I/O.
|
|
8
|
+
- Unused code: unused local variables, method parameters, private fields/methods, unreachable code, and unused imports/usings.
|
|
9
|
+
- Unnecessary allocations in hot paths (loops, frequently-called requests).
|
|
10
|
+
|
|
11
|
+
<!-- lang: cs,ts -->
|
|
12
|
+
## Performance
|
|
13
|
+
- Use of `StringBuilder` vs string concatenation in loops.
|
|
14
|
+
- Blocking synchronous operations where async should be used (I/O, network calls).
|
|
15
|
+
|
|
16
|
+
<!-- lang: cs,ts,sql -->
|
|
17
|
+
## Concurrency / Thread-safety
|
|
18
|
+
- Race conditions in shared code (static fields, singletons).
|
|
19
|
+
- Correct use of locks/semaphores, avoid deadlocks.
|
|
20
|
+
- Thread-safe collections where needed (`ConcurrentDictionary`, etc).
|
|
21
|
+
|
|
22
|
+
<!-- lang: all -->
|
|
23
|
+
## Configuration & Secrets
|
|
24
|
+
- Hardcoded values that should be in configuration (connection strings, URLs, timeouts).
|
|
25
|
+
- Secrets/API keys accidentally committed.
|
|
26
|
+
|
|
27
|
+
<!-- lang: cs,ts -->
|
|
28
|
+
## Dependencies & Compatibility
|
|
29
|
+
- New dependencies added without clear justification - unnecessary or outdated packages.
|
|
30
|
+
- Breaking changes when updating package versions.
|
|
31
|
+
|
|
32
|
+
<!-- lang: cs -->
|
|
33
|
+
## C#
|
|
34
|
+
- Naming Conventions & Casing: follow Microsoft's official C# naming guidelines (https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names).
|
|
35
|
+
- XML docs: follow Microsoft's official documentation guidance (https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/).
|
|
36
|
+
- Async: avoid `async void` (except handlers), use `ConfigureAwait(false)` in libraries, don't block with `.Result`/`.Wait()`.
|
|
37
|
+
- Nullable refs: correct `?` usage, avoid unjustified `!`.
|
|
38
|
+
- LINQ: avoid redundant iterations, N+1 queries with EF Core.
|
|
39
|
+
- Disposal: proper use of `IDisposable`/`using` for resources (streams, connections, etc).
|
|
40
|
+
- Immutability: prefer `readonly`, records, immutable types.
|
|
41
|
+
|
|
42
|
+
<!-- lang: cs,sql -->
|
|
43
|
+
## Logging & Observability
|
|
44
|
+
- Appropriate log levels (Debug vs Info vs Warning vs Error).
|
|
45
|
+
- Don't log sensitive data (PII, passwords, tokens).
|
|
46
|
+
|
|
47
|
+
<!-- lang: sql -->
|
|
48
|
+
## Migrations / Database (given your SQL/EF stack)
|
|
49
|
+
- Destructive changes to columns/tables with existing data (data loss risk).
|
|
50
|
+
- Migrations that lock large tables in production.
|
|
51
|
+
|
|
52
|
+
<!-- lang: sql -->
|
|
53
|
+
## SQL
|
|
54
|
+
- Avoid `SELECT *`; specify columns explicitly.
|
|
55
|
+
- Check for parameterized queries (avoid string concatenation → SQL injection).
|
|
56
|
+
- Indexes: flag queries likely to cause table scans on large tables.
|
|
57
|
+
- Transactions: ensure multi-step critical operations are wrapped in a transaction.
|
|
58
|
+
- Naming conventions for tables/columns consistent with the rest of the schema.
|
|
59
|
+
|
|
60
|
+
<!-- lang: xml -->
|
|
61
|
+
## XML
|
|
62
|
+
- Validation against schema (XSD) where applicable.
|
|
63
|
+
- Consistent formatting/indentation.
|
|
64
|
+
- Avoid duplicated configuration that could be centralized.
|
|
65
|
+
|
|
66
|
+
<!-- lang: html -->
|
|
67
|
+
## HTML
|
|
68
|
+
- A11y: `alt`, `aria-*`, labels on inputs, semantic tags (`<button>` vs `<div onclick>`).
|
|
69
|
+
- Semantic tags where they apply (`<button>`, `<label>`, `<ul>`/`<li>`) over generic `<div>`/`<span>`. Landmarks (`<header>`/`<nav>`/`<main>`) apply to shell/layout, not feature components.
|
|
70
|
+
- No inline `style=""` for static styling - use the component's `.less` file. `[style.x]`/`[ngStyle]` ok for dynamic values.
|
|
71
|
+
|
|
72
|
+
<!-- lang: ts,html -->
|
|
73
|
+
## TypeScript / Angular
|
|
74
|
+
- Follow the Angular style guide (https://angular.dev/style-guide).
|
|
75
|
+
- Avoid `any`, prefer explicit types.
|
|
76
|
+
- RxJS: unsubscribe via async pipe, `takeUntil`, or `takeUntilDestroyed`.
|
|
77
|
+
- Use `OnPush` where it makes sense.
|
|
78
|
+
- Separate presentation (component) from business logic (services); avoid heavy template logic.
|
|
79
|
+
- Files: kebab-case + type suffix (`select-maintenance-plan-step.component.ts/.html/.less`).
|
|
80
|
+
- Classes: PascalCase matching file name, no type suffix (`SelectMaintenancePlanStep`, not `...Component`).
|
|
81
|
+
- Selectors: kebab-case with project prefix (`app-select-maintenance-plan-step`).
|
|
82
|
+
- `.less` files named like their component; use `:host`, avoid global style leaks.
|
|
83
|
+
|
|
84
|
+
<!-- lang: all -->
|
|
85
|
+
## Review Process Notes (meta)
|
|
86
|
+
- Distinguish blocking comments (must-fix) from suggestions (nice-to-have) - this matters a lot for an automated reviewer, so it doesn't hold up merges over trivial issues.
|
|
87
|
+
- If the change is purely formatting/whitespace, do not comment on it.
|
|
88
|
+
- Ignore auto-generated files (generated migrations, `*.designer.cs`, `node_modules`, etc.) unless they contain manually-added code.
|
|
89
|
+
- Flag only issues relevant to the diff/change being reviewed, not the entire file, unless the change requires it.
|
|
90
|
+
- Before flagging something as "missing", verify it isn't present in the surrounding unchanged context (lines without `+`/`-`). Only flag genuinely absent elements, not elements outside the shown diff hunk.
|
|
91
|
+
- Priority order: Security > Functional bugs > Maintainability > Style.
|
|
@@ -17,6 +17,14 @@ def test_get_effective_model_prefers_explicit_value() -> None:
|
|
|
17
17
|
assert config.get_effective_model() == "custom-model"
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def test_review_config_has_no_review_language_attribute() -> None:
|
|
21
|
+
"""Regression: the configurable review language feature was removed."""
|
|
22
|
+
config = ReviewConfig()
|
|
23
|
+
|
|
24
|
+
assert hasattr(config, "review_language") is False
|
|
25
|
+
assert "review_language" not in ReviewConfig.__dataclass_fields__
|
|
26
|
+
|
|
27
|
+
|
|
20
28
|
@pytest.mark.parametrize(
|
|
21
29
|
("provider", "field", "expected"),
|
|
22
30
|
[
|
|
@@ -171,7 +179,6 @@ tfs:
|
|
|
171
179
|
ca_bundle: ~/ca.pem
|
|
172
180
|
repository: repo-a
|
|
173
181
|
review:
|
|
174
|
-
language: en
|
|
175
182
|
verbosity: quick
|
|
176
183
|
scope: full_code
|
|
177
184
|
max_diff_files: 12
|
|
@@ -215,7 +222,6 @@ output:
|
|
|
215
222
|
assert config.tfs_verify_ssl is False
|
|
216
223
|
assert config.tfs_ca_bundle == "~/ca.pem"
|
|
217
224
|
assert config.tfs_repository == "repo-a"
|
|
218
|
-
assert config.review_language == "en"
|
|
219
225
|
assert config.verbosity == "quick"
|
|
220
226
|
assert config.review_scope == "full_code"
|
|
221
227
|
assert config.max_diff_files == 12
|
|
@@ -10,7 +10,8 @@ from types import ModuleType, SimpleNamespace
|
|
|
10
10
|
import pytest
|
|
11
11
|
|
|
12
12
|
from src.config import ReviewConfig
|
|
13
|
-
from src
|
|
13
|
+
from src import llm_client
|
|
14
|
+
from src.llm_client import LLMClient, LLMError, build_user_message, get_scope_guidance, get_system_prompt, PR_COMMENT_PROMPT
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class FakeResponse:
|
|
@@ -79,7 +80,6 @@ def make_llm_config(**changes: object) -> ReviewConfig:
|
|
|
79
80
|
model="gpt-4o-mini",
|
|
80
81
|
max_tokens=256,
|
|
81
82
|
temperature=0.2,
|
|
82
|
-
review_language="en",
|
|
83
83
|
verbosity="detailed",
|
|
84
84
|
)
|
|
85
85
|
for key, value in changes.items():
|
|
@@ -89,88 +89,72 @@ def make_llm_config(**changes: object) -> ReviewConfig:
|
|
|
89
89
|
|
|
90
90
|
def test_prompt_helpers_select_expected_language_and_scope() -> None:
|
|
91
91
|
"""It should select prompts and scope guidance consistently."""
|
|
92
|
-
assert "code reviewer" in get_system_prompt("quick"
|
|
93
|
-
assert "JSON" in
|
|
94
|
-
assert "full_code" in get_scope_guidance("full_code"
|
|
95
|
-
assert "file
|
|
96
|
-
assert "added lines" in get_scope_guidance("diff_only"
|
|
92
|
+
assert "code reviewer" in get_system_prompt("quick").lower()
|
|
93
|
+
assert "JSON" in PR_COMMENT_PROMPT
|
|
94
|
+
assert "full_code" in get_scope_guidance("full_code")
|
|
95
|
+
assert "file and line" in get_scope_guidance("diff_only", structured=True).lower()
|
|
96
|
+
assert "added lines" in get_scope_guidance("diff_only")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_get_pr_comment_prompt_function_removed() -> None:
|
|
100
|
+
"""Regression: get_pr_comment_prompt() was replaced by PR_COMMENT_PROMPT."""
|
|
101
|
+
assert not hasattr(llm_client, "get_pr_comment_prompt")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_get_system_prompt_and_get_scope_guidance_reject_language_kwarg() -> None:
|
|
105
|
+
"""Regression: language parameter was removed from both prompt helpers."""
|
|
106
|
+
with pytest.raises(TypeError):
|
|
107
|
+
get_system_prompt("quick", language="pt") # type: ignore[call-arg]
|
|
108
|
+
|
|
109
|
+
with pytest.raises(TypeError):
|
|
110
|
+
get_scope_guidance("diff_only", structured=False, language="pt") # type: ignore[call-arg]
|
|
111
|
+
|
|
97
112
|
|
|
98
113
|
|
|
99
114
|
# ---------------------------------------------------------------------------
|
|
100
115
|
# SYSTEM_PROMPTS / get_system_prompt — Phase 1 change validation
|
|
101
116
|
# ---------------------------------------------------------------------------
|
|
102
117
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
prompt = get_system_prompt("quick", language)
|
|
118
|
+
def test_get_system_prompt_quick_returns_non_empty_string() -> None:
|
|
119
|
+
"""quick prompt must return a non-empty string."""
|
|
120
|
+
prompt = get_system_prompt("quick")
|
|
107
121
|
assert isinstance(prompt, str)
|
|
108
122
|
assert len(prompt.strip()) > 0
|
|
109
123
|
|
|
110
124
|
|
|
111
|
-
def
|
|
112
|
-
"""detailed
|
|
113
|
-
prompt = get_system_prompt("detailed"
|
|
125
|
+
def test_get_system_prompt_detailed_enforces_inline_comment_format() -> None:
|
|
126
|
+
"""detailed must specify the inline comment output format after Phase 1."""
|
|
127
|
+
prompt = get_system_prompt("detailed")
|
|
114
128
|
# Must instruct the LLM to return only inline comments
|
|
115
129
|
assert "inline comments" in prompt.lower()
|
|
116
130
|
# Must include the exact per-line format marker
|
|
117
131
|
assert "Line <line_number>" in prompt
|
|
118
132
|
|
|
119
133
|
|
|
120
|
-
def
|
|
121
|
-
"""detailed
|
|
122
|
-
prompt = get_system_prompt("detailed"
|
|
134
|
+
def test_get_system_prompt_detailed_forbids_summary_sections() -> None:
|
|
135
|
+
"""detailed must explicitly forbid summary sections after Phase 1."""
|
|
136
|
+
prompt = get_system_prompt("detailed")
|
|
123
137
|
assert "Do NOT produce summary sections" in prompt
|
|
124
138
|
|
|
125
139
|
|
|
126
|
-
def
|
|
127
|
-
"""detailed
|
|
128
|
-
prompt = get_system_prompt("detailed"
|
|
129
|
-
assert "focus on" not in prompt.lower()
|
|
130
|
-
assert "structured review" not in prompt.lower()
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def test_get_system_prompt_detailed_pt_enforces_inline_comment_format() -> None:
|
|
134
|
-
"""detailed/pt must specify the inline comment output format in Portuguese after Phase 1."""
|
|
135
|
-
prompt = get_system_prompt("detailed", "pt")
|
|
136
|
-
assert "inline" in prompt.lower()
|
|
137
|
-
# Must include the per-line format marker in Portuguese
|
|
138
|
-
assert "Linha" in prompt
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def test_get_system_prompt_detailed_pt_forbids_summary_sections() -> None:
|
|
142
|
-
"""detailed/pt must explicitly forbid summary sections in Portuguese after Phase 1."""
|
|
143
|
-
prompt = get_system_prompt("detailed", "pt")
|
|
144
|
-
assert "NÃO produzas" in prompt
|
|
145
|
-
# The forbidden construct must mention summary sections
|
|
146
|
-
assert "sumário" in prompt.lower() or "secções" in prompt.lower()
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def test_get_system_prompt_detailed_pt_contains_no_focus_on_or_structured_review_language() -> None:
|
|
150
|
-
"""detailed/pt must NOT contain generic 'Focus on' or 'structured review' language after Phase 1."""
|
|
151
|
-
prompt = get_system_prompt("detailed", "pt")
|
|
140
|
+
def test_get_system_prompt_detailed_contains_no_focus_on_or_structured_review_language() -> None:
|
|
141
|
+
"""detailed must NOT contain generic 'Focus on' or 'structured review' language after Phase 1."""
|
|
142
|
+
prompt = get_system_prompt("detailed")
|
|
152
143
|
assert "focus on" not in prompt.lower()
|
|
153
144
|
assert "structured review" not in prompt.lower()
|
|
154
145
|
|
|
155
146
|
|
|
156
|
-
def
|
|
157
|
-
"""security
|
|
158
|
-
prompt = get_system_prompt("security"
|
|
147
|
+
def test_get_system_prompt_security_mentions_security_specific_terms() -> None:
|
|
148
|
+
"""security must include core vulnerability types it is expected to check for."""
|
|
149
|
+
prompt = get_system_prompt("security")
|
|
159
150
|
for term in ("SQL Injection", "XSS", "CSRF"):
|
|
160
151
|
assert term in prompt, f"Security prompt missing expected term: '{term}'"
|
|
161
152
|
|
|
162
153
|
|
|
163
154
|
def test_get_system_prompt_unknown_verbosity_falls_back_to_detailed() -> None:
|
|
164
155
|
"""An unrecognised verbosity level must silently fall back to the 'detailed' prompt."""
|
|
165
|
-
fallback = get_system_prompt("unknown_verbosity"
|
|
166
|
-
expected = get_system_prompt("detailed"
|
|
167
|
-
assert fallback == expected
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def test_get_system_prompt_unknown_language_falls_back_to_pt() -> None:
|
|
171
|
-
"""An unrecognised language code must fall back to the Portuguese ('pt') prompt."""
|
|
172
|
-
fallback = get_system_prompt("detailed", "xx")
|
|
173
|
-
expected = get_system_prompt("detailed", "pt")
|
|
156
|
+
fallback = get_system_prompt("unknown_verbosity")
|
|
157
|
+
expected = get_system_prompt("detailed")
|
|
174
158
|
assert fallback == expected
|
|
175
159
|
|
|
176
160
|
|
|
@@ -179,39 +163,35 @@ def test_get_system_prompt_unknown_language_falls_back_to_pt() -> None:
|
|
|
179
163
|
# ---------------------------------------------------------------------------
|
|
180
164
|
|
|
181
165
|
def test_get_scope_guidance_diff_only_references_full_file_context_markers() -> None:
|
|
182
|
-
"""diff_only guidance must reference FULL_FILE_CONTEXT_START and _END for
|
|
183
|
-
for
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
assert "FULL_FILE_CONTEXT_END" in guidance, f"lang={lang} structured={structured}"
|
|
166
|
+
"""diff_only guidance must reference FULL_FILE_CONTEXT_START and _END for both modes."""
|
|
167
|
+
for structured in (False, True):
|
|
168
|
+
guidance = get_scope_guidance("diff_only", structured=structured)
|
|
169
|
+
assert "FULL_FILE_CONTEXT_START" in guidance, f"structured={structured}"
|
|
170
|
+
assert "FULL_FILE_CONTEXT_END" in guidance, f"structured={structured}"
|
|
188
171
|
|
|
189
172
|
|
|
190
|
-
|
|
191
|
-
def test_get_scope_guidance_diff_only_structured_demands_valid_line_number(language: str) -> None:
|
|
173
|
+
def test_get_scope_guidance_diff_only_structured_demands_valid_line_number() -> None:
|
|
192
174
|
"""Structured diff_only guidance must instruct the LLM to supply a line number > 0."""
|
|
193
|
-
guidance = get_scope_guidance("diff_only",
|
|
175
|
+
guidance = get_scope_guidance("diff_only", structured=True)
|
|
194
176
|
assert "(>0)" in guidance
|
|
195
177
|
|
|
196
178
|
|
|
197
179
|
def test_get_scope_guidance_diff_only_non_structured_focuses_on_changed_lines() -> None:
|
|
198
180
|
"""Non-structured diff_only guidance must tell the LLM to focus on changed/added lines."""
|
|
199
|
-
guidance_en = get_scope_guidance("diff_only",
|
|
200
|
-
guidance_pt = get_scope_guidance("diff_only", "pt", structured=False)
|
|
181
|
+
guidance_en = get_scope_guidance("diff_only", structured=False)
|
|
201
182
|
|
|
202
183
|
assert "changed lines" in guidance_en.lower() or "added lines" in guidance_en.lower()
|
|
203
|
-
assert "alteradas" in guidance_pt.lower() or "adicionadas" in guidance_pt.lower()
|
|
204
184
|
|
|
205
185
|
|
|
206
186
|
def test_get_scope_guidance_diff_only_structured_does_not_demand_line_in_non_structured() -> None:
|
|
207
187
|
"""Non-structured diff_only guidance must NOT mandate a minimum line number."""
|
|
208
|
-
guidance_en = get_scope_guidance("diff_only",
|
|
188
|
+
guidance_en = get_scope_guidance("diff_only", structured=False)
|
|
209
189
|
assert "(>0)" not in guidance_en
|
|
210
190
|
|
|
211
191
|
|
|
212
192
|
def test_get_scope_guidance_unknown_scope_falls_back_to_diff_only() -> None:
|
|
213
193
|
"""An unrecognised review_scope must fall through and return diff_only guidance."""
|
|
214
|
-
guidance = get_scope_guidance("unknown_scope"
|
|
194
|
+
guidance = get_scope_guidance("unknown_scope")
|
|
215
195
|
|
|
216
196
|
assert "diff_only" in guidance
|
|
217
197
|
assert "FULL_FILE_CONTEXT_START" in guidance
|
|
@@ -267,6 +247,120 @@ def test_review_pr_dispatches_and_merges_custom_prompt(mocker, tmp_path: Path) -
|
|
|
267
247
|
assert "Custom user instructions" in system_prompt
|
|
268
248
|
assert "Custom context loaded from" in user_message
|
|
269
249
|
|
|
250
|
+
def test_review_omits_custom_instructions_header_when_filter_empties_prompt(mocker, tmp_path: Path) -> None:
|
|
251
|
+
"""If filtering removes all sections, the 'Custom user instructions' header should not appear."""
|
|
252
|
+
prompt_file = tmp_path / "prompt.md"
|
|
253
|
+
prompt_file.write_text(
|
|
254
|
+
"<!-- lang: java -->\n"
|
|
255
|
+
"## Java Rules\n"
|
|
256
|
+
"- Use streams.\n",
|
|
257
|
+
encoding="utf-8",
|
|
258
|
+
)
|
|
259
|
+
config = make_llm_config(custom_prompt_file=str(prompt_file))
|
|
260
|
+
client = LLMClient(config)
|
|
261
|
+
openai = mocker.patch("src.llm_client.LLMClient._call_openai", return_value="review text")
|
|
262
|
+
|
|
263
|
+
client.review("+code", [{"file": "component.ts", "additions": 1, "deletions": 0}])
|
|
264
|
+
|
|
265
|
+
system_prompt, user_message = openai.call_args.args[:2]
|
|
266
|
+
assert "Custom user instructions" not in system_prompt
|
|
267
|
+
assert "Custom context loaded from" not in user_message
|
|
268
|
+
|
|
269
|
+
def test_review_filters_custom_prompt_sections_by_changed_extensions(mocker, tmp_path: Path) -> None:
|
|
270
|
+
"""Only sections matching the extensions of changed files (plus 'all') should be kept."""
|
|
271
|
+
prompt_file = tmp_path / "prompt.md"
|
|
272
|
+
prompt_file.write_text(
|
|
273
|
+
"<!-- lang: all -->\n"
|
|
274
|
+
"## General\n"
|
|
275
|
+
"- Always check for secrets.\n"
|
|
276
|
+
"\n"
|
|
277
|
+
"<!-- lang: cs -->\n"
|
|
278
|
+
"## CSharp Rules\n"
|
|
279
|
+
"- Avoid magic strings.\n"
|
|
280
|
+
"\n"
|
|
281
|
+
"<!-- lang: py -->\n"
|
|
282
|
+
"## Python Rules\n"
|
|
283
|
+
"- Use type hints.\n",
|
|
284
|
+
encoding="utf-8",
|
|
285
|
+
)
|
|
286
|
+
config = make_llm_config(custom_prompt_file=str(prompt_file))
|
|
287
|
+
client = LLMClient(config)
|
|
288
|
+
openai = mocker.patch("src.llm_client.LLMClient._call_openai", return_value="review text")
|
|
289
|
+
|
|
290
|
+
client.review("+code", [{"file": "app.py", "additions": 1, "deletions": 0}])
|
|
291
|
+
|
|
292
|
+
system_prompt, _ = openai.call_args.args[:2]
|
|
293
|
+
assert "General" in system_prompt
|
|
294
|
+
assert "Python Rules" in system_prompt
|
|
295
|
+
assert "CSharp Rules" not in system_prompt
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_review_keeps_only_all_sections_when_no_matching_extension(mocker, tmp_path: Path) -> None:
|
|
299
|
+
"""If no changed file matches a tagged section, only 'all' sections remain."""
|
|
300
|
+
prompt_file = tmp_path / "prompt.md"
|
|
301
|
+
prompt_file.write_text(
|
|
302
|
+
"<!-- lang: all -->\n"
|
|
303
|
+
"## General\n"
|
|
304
|
+
"- Always check for secrets.\n"
|
|
305
|
+
"\n"
|
|
306
|
+
"<!-- lang: java -->\n"
|
|
307
|
+
"## Java Rules\n"
|
|
308
|
+
"- Use streams.\n",
|
|
309
|
+
encoding="utf-8",
|
|
310
|
+
)
|
|
311
|
+
config = make_llm_config(custom_prompt_file=str(prompt_file))
|
|
312
|
+
client = LLMClient(config)
|
|
313
|
+
openai = mocker.patch("src.llm_client.LLMClient._call_openai", return_value="review text")
|
|
314
|
+
|
|
315
|
+
client.review("+code", [{"file": "component.ts", "additions": 1, "deletions": 0}])
|
|
316
|
+
|
|
317
|
+
system_prompt, _ = openai.call_args.args[:2]
|
|
318
|
+
assert "General" in system_prompt
|
|
319
|
+
assert "Java Rules" not in system_prompt
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def test_review_includes_multiple_extension_sections(mocker, tmp_path: Path) -> None:
|
|
323
|
+
"""Changed files with different extensions should each pull in their matching section."""
|
|
324
|
+
prompt_file = tmp_path / "prompt.md"
|
|
325
|
+
prompt_file.write_text(
|
|
326
|
+
"<!-- lang: ts -->\n"
|
|
327
|
+
"## TypeScript Rules\n"
|
|
328
|
+
"- No any types.\n"
|
|
329
|
+
"\n"
|
|
330
|
+
"<!-- lang: html -->\n"
|
|
331
|
+
"## Html Rules\n"
|
|
332
|
+
"- No inline styles.\n",
|
|
333
|
+
encoding="utf-8",
|
|
334
|
+
)
|
|
335
|
+
config = make_llm_config(custom_prompt_file=str(prompt_file))
|
|
336
|
+
client = LLMClient(config)
|
|
337
|
+
openai = mocker.patch("src.llm_client.LLMClient._call_openai", return_value="review text")
|
|
338
|
+
|
|
339
|
+
client.review(
|
|
340
|
+
"+code",
|
|
341
|
+
[
|
|
342
|
+
{"file": "foo.component.ts", "additions": 1, "deletions": 0},
|
|
343
|
+
{"file": "foo.component.html", "additions": 1, "deletions": 0},
|
|
344
|
+
],
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
system_prompt, _ = openai.call_args.args[:2]
|
|
348
|
+
assert "TypeScript Rules" in system_prompt
|
|
349
|
+
assert "Html Rules" in system_prompt
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_review_without_lang_tags_keeps_full_custom_prompt(mocker, tmp_path: Path) -> None:
|
|
353
|
+
"""Custom prompt with no <!-- lang: --> tags at all should pass through unchanged."""
|
|
354
|
+
prompt_file = tmp_path / "prompt.md"
|
|
355
|
+
prompt_file.write_text("Always mention tests", encoding="utf-8")
|
|
356
|
+
config = make_llm_config(custom_prompt_file=str(prompt_file))
|
|
357
|
+
client = LLMClient(config)
|
|
358
|
+
openai = mocker.patch("src.llm_client.LLMClient._call_openai", return_value="review text")
|
|
359
|
+
|
|
360
|
+
client.review("+code", [{"file": "a.py", "additions": 1, "deletions": 0}])
|
|
361
|
+
|
|
362
|
+
system_prompt, _ = openai.call_args.args[:2]
|
|
363
|
+
assert "Always mention tests" in system_prompt
|
|
270
364
|
|
|
271
365
|
def test_review_pr_raises_for_unsupported_provider() -> None:
|
|
272
366
|
"""It should reject unsupported providers before any HTTP call."""
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for prompt_utils.py - language/extension based filtering of the custom review prompt.
|
|
3
|
+
"""
|
|
4
|
+
from src.prompt_utils import detect_langs, filter_prompt_by_langs
|
|
5
|
+
|
|
6
|
+
class TestDetectLangs:
|
|
7
|
+
|
|
8
|
+
def test_always_includes_all(self) -> None:
|
|
9
|
+
"""'all' must always be present, even with no files."""
|
|
10
|
+
assert detect_langs([]) == {"all"}
|
|
11
|
+
|
|
12
|
+
def test_detects_single_extension(self) -> None:
|
|
13
|
+
result = detect_langs(["src/foo.py"])
|
|
14
|
+
assert result == {"all", "py"}
|
|
15
|
+
|
|
16
|
+
def test_detects_multiple_distinct_extensions(self) -> None:
|
|
17
|
+
result = detect_langs(["a.ts", "b.html", "c.cs"])
|
|
18
|
+
assert result == {"all", "ts", "html", "cs"}
|
|
19
|
+
|
|
20
|
+
def test_deduplicates_same_extension(self) -> None:
|
|
21
|
+
result = detect_langs(["a.py", "b.py", "c.py"])
|
|
22
|
+
assert result == {"all", "py"}
|
|
23
|
+
|
|
24
|
+
def test_extension_lowercased(self) -> None:
|
|
25
|
+
result = detect_langs(["Component.TS"])
|
|
26
|
+
assert result == {"all", "ts"}
|
|
27
|
+
|
|
28
|
+
def test_ignores_none_or_empty_paths(self) -> None:
|
|
29
|
+
result = detect_langs([None, "", "a.py"])
|
|
30
|
+
assert result == {"all", "py"}
|
|
31
|
+
|
|
32
|
+
def test_file_without_extension_is_ignored(self) -> None:
|
|
33
|
+
result = detect_langs(["Dockerfile", "Makefile"])
|
|
34
|
+
assert result == {"all"}
|
|
35
|
+
|
|
36
|
+
def test_nested_path_extracts_extension_correctly(self) -> None:
|
|
37
|
+
result = detect_langs(["src/app/components/foo.component.ts"])
|
|
38
|
+
assert result == {"all", "ts"}
|
|
39
|
+
|
|
40
|
+
def test_dotfile_with_no_further_extension_is_ignored(self) -> None:
|
|
41
|
+
# os.path.splitext(".gitignore") -> ('.gitignore', '') -> no extension detected
|
|
42
|
+
result = detect_langs([".gitignore"])
|
|
43
|
+
assert result == {"all"}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestFilterPromptByLangs:
|
|
47
|
+
|
|
48
|
+
def test_content_without_any_tag_is_kept_entirely(self) -> None:
|
|
49
|
+
content = "Always mention tests"
|
|
50
|
+
result = filter_prompt_by_langs(content, {"all"})
|
|
51
|
+
assert result == "Always mention tests"
|
|
52
|
+
|
|
53
|
+
def test_all_section_is_always_included(self) -> None:
|
|
54
|
+
content = (
|
|
55
|
+
"<!-- lang: all -->\n"
|
|
56
|
+
"## General\n"
|
|
57
|
+
"- Rule A\n"
|
|
58
|
+
)
|
|
59
|
+
result = filter_prompt_by_langs(content, {"all", "cs"})
|
|
60
|
+
assert "## General" in result
|
|
61
|
+
assert "Rule A" in result
|
|
62
|
+
|
|
63
|
+
def test_matching_lang_section_is_included(self) -> None:
|
|
64
|
+
content = (
|
|
65
|
+
"<!-- lang: cs,ts -->\n"
|
|
66
|
+
"## Dependencies\n"
|
|
67
|
+
"- Rule B\n"
|
|
68
|
+
)
|
|
69
|
+
result = filter_prompt_by_langs(content, {"all", "ts"})
|
|
70
|
+
assert "## Dependencies" in result
|
|
71
|
+
assert "Rule B" in result
|
|
72
|
+
|
|
73
|
+
def test_non_matching_lang_section_is_excluded(self) -> None:
|
|
74
|
+
content = (
|
|
75
|
+
"<!-- lang: java -->\n"
|
|
76
|
+
"## Java Rules\n"
|
|
77
|
+
"- Rule C\n"
|
|
78
|
+
)
|
|
79
|
+
result = filter_prompt_by_langs(content, {"all", "ts"})
|
|
80
|
+
assert "Java Rules" not in result
|
|
81
|
+
assert "Rule C" not in result
|
|
82
|
+
|
|
83
|
+
def test_mixed_sections_filters_correctly(self) -> None:
|
|
84
|
+
content = (
|
|
85
|
+
"<!-- lang: all -->\n"
|
|
86
|
+
"## General\n"
|
|
87
|
+
"- General rule\n"
|
|
88
|
+
"\n"
|
|
89
|
+
"<!-- lang: cs -->\n"
|
|
90
|
+
"## CSharp\n"
|
|
91
|
+
"- CSharp rule\n"
|
|
92
|
+
"\n"
|
|
93
|
+
"<!-- lang: html -->\n"
|
|
94
|
+
"## Html\n"
|
|
95
|
+
"- Html rule\n"
|
|
96
|
+
)
|
|
97
|
+
result = filter_prompt_by_langs(content, {"all", "html"})
|
|
98
|
+
|
|
99
|
+
assert "General rule" in result
|
|
100
|
+
assert "Html rule" in result
|
|
101
|
+
assert "CSharp rule" not in result
|
|
102
|
+
assert "CSharp" not in result
|
|
103
|
+
|
|
104
|
+
def test_tags_are_case_insensitive(self) -> None:
|
|
105
|
+
content = (
|
|
106
|
+
"<!-- lang: CS,TS -->\n"
|
|
107
|
+
"## Dependencies\n"
|
|
108
|
+
"- Rule\n"
|
|
109
|
+
)
|
|
110
|
+
result = filter_prompt_by_langs(content, {"all", "cs"})
|
|
111
|
+
assert "Rule" in result
|
|
112
|
+
|
|
113
|
+
def test_no_matching_section_returns_empty(self) -> None:
|
|
114
|
+
content = (
|
|
115
|
+
"<!-- lang: java -->\n"
|
|
116
|
+
"## Java Rules\n"
|
|
117
|
+
"- Rule\n"
|
|
118
|
+
)
|
|
119
|
+
result = filter_prompt_by_langs(content, {"all", "ts"})
|
|
120
|
+
assert result == ""
|
|
121
|
+
|
|
122
|
+
def test_tag_with_extra_whitespace_is_parsed(self) -> None:
|
|
123
|
+
content = (
|
|
124
|
+
"<!-- lang: cs , ts -->\n"
|
|
125
|
+
"## Dependencies\n"
|
|
126
|
+
"- Rule\n"
|
|
127
|
+
)
|
|
128
|
+
result = filter_prompt_by_langs(content, {"all", "ts"})
|
|
129
|
+
assert "Rule" in result
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
- Naming Conventions & Casing, follow Microsoft's official C# naming guidelines (https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names).
|
|
2
|
-
- XML Documentation Comments, follow Microsoft's official code documentation (https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/).
|
|
File without changes
|
{code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{code_review_ai_cli-1.3.4 → code_review_ai_cli-1.3.6}/code_review_ai_cli.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|