code-review-ai-cli 1.3.5__tar.gz → 2.0.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 (28) hide show
  1. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/PKG-INFO +8 -26
  2. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/README.md +7 -25
  3. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/code_review_ai_cli.egg-info/PKG-INFO +8 -26
  4. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/code_review_ai_cli.egg-info/SOURCES.txt +2 -0
  5. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/pyproject.toml +1 -1
  6. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/src/git_utils.py +0 -1
  7. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/src/llm_client.py +7 -0
  8. code_review_ai_cli-2.0.0/src/prompt_utils.py +56 -0
  9. code_review_ai_cli-2.0.0/src/prompts/review_prompt.md.template +91 -0
  10. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/tests/test_llm_client.py +114 -0
  11. code_review_ai_cli-2.0.0/tests/test_prompt_utils.py +129 -0
  12. code_review_ai_cli-1.3.5/src/prompts/review_prompt.md.template +0 -2
  13. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/code_review_ai_cli.egg-info/dependency_links.txt +0 -0
  14. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/code_review_ai_cli.egg-info/entry_points.txt +0 -0
  15. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/code_review_ai_cli.egg-info/requires.txt +0 -0
  16. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/code_review_ai_cli.egg-info/top_level.txt +0 -0
  17. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/setup.cfg +0 -0
  18. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/src/__init__.py +0 -0
  19. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/src/ai_review.py +0 -0
  20. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/src/config.py +0 -0
  21. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/src/formatter.py +0 -0
  22. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/src/prompts/config.yaml.template +0 -0
  23. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/src/tfs_client.py +0 -0
  24. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/tests/test_ai_review.py +0 -0
  25. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/tests/test_config.py +0 -0
  26. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/tests/test_formatter.py +0 -0
  27. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/tests/test_git_utils.py +0 -0
  28. {code_review_ai_cli-1.3.5 → code_review_ai_cli-2.0.0}/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.5
3
+ Version: 2.0.0
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
@@ -89,19 +89,6 @@ This copies two bundled templates:
89
89
  - **`config.yaml`** — all available options with inline documentation
90
90
  - **`review_prompt.md`** — default review style rules, injected into every LLM prompt
91
91
 
92
- ```text
93
- ✅ config.yaml created at: /home/user/my-project/config.yaml
94
- ✅ review_prompt.md created at: /home/user/my-project/review_prompt.md
95
- Edit them to add your credentials, preferences and review rules.
96
- ```
97
-
98
- If either file already exists you will be prompted individually before it is overwritten:
99
-
100
- ```text
101
- config.yaml already exists in the current directory.
102
- Overwrite? [y/N]
103
- ```
104
-
105
92
  The tool looks for `config.yaml` in the **current working directory** at runtime. You can also pass a different path with `--config`:
106
93
 
107
94
  ```bash
@@ -156,7 +143,7 @@ output:
156
143
  | `diff_only` | Default. Unified diff (changed lines only) + full file content as read-only context. |
157
144
  | `full_code` | All lines of the new file version, every line prefixed with `+`. No baseline. |
158
145
 
159
- #### `diff_only` — diff with full-file context (default)
146
+ #### `diff_only` — diff with context (default)
160
147
 
161
148
  ```yaml
162
149
  review:
@@ -164,16 +151,6 @@ review:
164
151
  scope: full_code
165
152
  ```
166
153
 
167
- When `diff_only` is active, the tool:
168
-
169
- 1. Generates a standard unified diff (added/removed lines with 3 lines of surrounding context) for each changed file.
170
- 2. Appends the **complete new-version file content** as a clearly-marked, read-only context block immediately after the diff for that file:
171
- 3. Strips all context lines and deleted lines (`-`) before sending to the LLM, so only added lines (`+`) and structural headers remain in the diff section.
172
- 4. Instructs the LLM (via the system prompt) to **use the full-file block as read-only background** and to focus the review exclusively on the `+` lines.
173
-
174
- #### `full_code` — entire new file as added lines
175
- Every line of the new file version is prefixed with `+` and sent without a `-` baseline. The LLM receives the complete content and is asked to review only the new code. This is more expensive and slower but may catch issues in unchanged lines that are affected by the changes.
176
-
177
154
  ### Filter by File Extension
178
155
 
179
156
  `file_extensions_filter` works as an **allowlist**: only files with listed extensions are sent to the LLM for review. Remaining files are excluded from the diff before any processing.
@@ -204,7 +181,12 @@ Edit it to tailor the review to your team:
204
181
  - Include business/architecture context
205
182
  - Add examples of good/bad comments
206
183
 
207
- The path is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
184
+ **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:
185
+
186
+ - Rules marked with `<!-- lang: all -->` for all files.
187
+ - Rules matching the extensions of the files being reviewed. Example `<!-- lang: cs,ts -->` applied when .cs or .ts files are present.
188
+
189
+ The path for the **markdown-customizable prompt** is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
208
190
 
209
191
  ```yaml
210
192
  review:
@@ -52,19 +52,6 @@ This copies two bundled templates:
52
52
  - **`config.yaml`** — all available options with inline documentation
53
53
  - **`review_prompt.md`** — default review style rules, injected into every LLM prompt
54
54
 
55
- ```text
56
- ✅ config.yaml created at: /home/user/my-project/config.yaml
57
- ✅ review_prompt.md created at: /home/user/my-project/review_prompt.md
58
- Edit them to add your credentials, preferences and review rules.
59
- ```
60
-
61
- If either file already exists you will be prompted individually before it is overwritten:
62
-
63
- ```text
64
- config.yaml already exists in the current directory.
65
- Overwrite? [y/N]
66
- ```
67
-
68
55
  The tool looks for `config.yaml` in the **current working directory** at runtime. You can also pass a different path with `--config`:
69
56
 
70
57
  ```bash
@@ -119,7 +106,7 @@ output:
119
106
  | `diff_only` | Default. Unified diff (changed lines only) + full file content as read-only context. |
120
107
  | `full_code` | All lines of the new file version, every line prefixed with `+`. No baseline. |
121
108
 
122
- #### `diff_only` — diff with full-file context (default)
109
+ #### `diff_only` — diff with context (default)
123
110
 
124
111
  ```yaml
125
112
  review:
@@ -127,16 +114,6 @@ review:
127
114
  scope: full_code
128
115
  ```
129
116
 
130
- When `diff_only` is active, the tool:
131
-
132
- 1. Generates a standard unified diff (added/removed lines with 3 lines of surrounding context) for each changed file.
133
- 2. Appends the **complete new-version file content** as a clearly-marked, read-only context block immediately after the diff for that file:
134
- 3. Strips all context lines and deleted lines (`-`) before sending to the LLM, so only added lines (`+`) and structural headers remain in the diff section.
135
- 4. Instructs the LLM (via the system prompt) to **use the full-file block as read-only background** and to focus the review exclusively on the `+` lines.
136
-
137
- #### `full_code` — entire new file as added lines
138
- Every line of the new file version is prefixed with `+` and sent without a `-` baseline. The LLM receives the complete content and is asked to review only the new code. This is more expensive and slower but may catch issues in unchanged lines that are affected by the changes.
139
-
140
117
  ### Filter by File Extension
141
118
 
142
119
  `file_extensions_filter` works as an **allowlist**: only files with listed extensions are sent to the LLM for review. Remaining files are excluded from the diff before any processing.
@@ -167,7 +144,12 @@ Edit it to tailor the review to your team:
167
144
  - Include business/architecture context
168
145
  - Add examples of good/bad comments
169
146
 
170
- The path is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
147
+ **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:
148
+
149
+ - Rules marked with `<!-- lang: all -->` for all files.
150
+ - Rules matching the extensions of the files being reviewed. Example `<!-- lang: cs,ts -->` applied when .cs or .ts files are present.
151
+
152
+ The path for the **markdown-customizable prompt** is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
171
153
 
172
154
  ```yaml
173
155
  review:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-review-ai-cli
3
- Version: 1.3.5
3
+ Version: 2.0.0
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
@@ -89,19 +89,6 @@ This copies two bundled templates:
89
89
  - **`config.yaml`** — all available options with inline documentation
90
90
  - **`review_prompt.md`** — default review style rules, injected into every LLM prompt
91
91
 
92
- ```text
93
- ✅ config.yaml created at: /home/user/my-project/config.yaml
94
- ✅ review_prompt.md created at: /home/user/my-project/review_prompt.md
95
- Edit them to add your credentials, preferences and review rules.
96
- ```
97
-
98
- If either file already exists you will be prompted individually before it is overwritten:
99
-
100
- ```text
101
- config.yaml already exists in the current directory.
102
- Overwrite? [y/N]
103
- ```
104
-
105
92
  The tool looks for `config.yaml` in the **current working directory** at runtime. You can also pass a different path with `--config`:
106
93
 
107
94
  ```bash
@@ -156,7 +143,7 @@ output:
156
143
  | `diff_only` | Default. Unified diff (changed lines only) + full file content as read-only context. |
157
144
  | `full_code` | All lines of the new file version, every line prefixed with `+`. No baseline. |
158
145
 
159
- #### `diff_only` — diff with full-file context (default)
146
+ #### `diff_only` — diff with context (default)
160
147
 
161
148
  ```yaml
162
149
  review:
@@ -164,16 +151,6 @@ review:
164
151
  scope: full_code
165
152
  ```
166
153
 
167
- When `diff_only` is active, the tool:
168
-
169
- 1. Generates a standard unified diff (added/removed lines with 3 lines of surrounding context) for each changed file.
170
- 2. Appends the **complete new-version file content** as a clearly-marked, read-only context block immediately after the diff for that file:
171
- 3. Strips all context lines and deleted lines (`-`) before sending to the LLM, so only added lines (`+`) and structural headers remain in the diff section.
172
- 4. Instructs the LLM (via the system prompt) to **use the full-file block as read-only background** and to focus the review exclusively on the `+` lines.
173
-
174
- #### `full_code` — entire new file as added lines
175
- Every line of the new file version is prefixed with `+` and sent without a `-` baseline. The LLM receives the complete content and is asked to review only the new code. This is more expensive and slower but may catch issues in unchanged lines that are affected by the changes.
176
-
177
154
  ### Filter by File Extension
178
155
 
179
156
  `file_extensions_filter` works as an **allowlist**: only files with listed extensions are sent to the LLM for review. Remaining files are excluded from the diff before any processing.
@@ -204,7 +181,12 @@ Edit it to tailor the review to your team:
204
181
  - Include business/architecture context
205
182
  - Add examples of good/bad comments
206
183
 
207
- The path is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
184
+ **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:
185
+
186
+ - Rules marked with `<!-- lang: all -->` for all files.
187
+ - Rules matching the extensions of the files being reviewed. Example `<!-- lang: cs,ts -->` applied when .cs or .ts files are present.
188
+
189
+ The path for the **markdown-customizable prompt** is configurable in `config.yaml` (default: `review_prompt.md` in the current directory):
208
190
 
209
191
  ```yaml
210
192
  review:
@@ -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.5"
7
+ version = "2.0.0"
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"
@@ -14,7 +14,6 @@ import subprocess
14
14
  import os
15
15
  from typing import Optional
16
16
 
17
-
18
17
  class GitError(Exception):
19
18
  """Exception for Git-related errors."""
20
19
  pass
@@ -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):
@@ -301,6 +302,12 @@ class LLMClient:
301
302
  json_schema_prompt = PR_COMMENT_PROMPT
302
303
  custom_prompt = self._load_custom_prompt_text()
303
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
+
304
311
  scope_guidance = get_scope_guidance(
305
312
  review_scope=review_scope,
306
313
  structured=True,
@@ -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.
@@ -247,6 +247,120 @@ def test_review_pr_dispatches_and_merges_custom_prompt(mocker, tmp_path: Path) -
247
247
  assert "Custom user instructions" in system_prompt
248
248
  assert "Custom context loaded from" in user_message
249
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='{"summary": "review text", "comments": []}')
262
+
263
+ client.review_pr("+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='{"summary": "review text", "comments": []}')
289
+
290
+ client.review_pr("+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='{"summary": "review text", "comments": []}')
314
+
315
+ client.review_pr("+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='{"summary": "review text", "comments": []}')
338
+
339
+ client.review_pr(
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='{"summary": "review text", "comments": []}')
359
+
360
+ client.review_pr("+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
250
364
 
251
365
  def test_review_pr_raises_for_unsupported_provider() -> None:
252
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/).