code-review-ai-cli 2.2.0__tar.gz → 2.2.2__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-2.2.0 → code_review_ai_cli-2.2.2}/PKG-INFO +1 -1
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/code_review_ai_cli.egg-info/PKG-INFO +1 -1
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/pyproject.toml +1 -1
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/tfs_client.py +21 -10
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/tests/test_tfs_client.py +120 -8
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/README.md +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/code_review_ai_cli.egg-info/SOURCES.txt +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/code_review_ai_cli.egg-info/dependency_links.txt +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/code_review_ai_cli.egg-info/entry_points.txt +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/code_review_ai_cli.egg-info/requires.txt +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/code_review_ai_cli.egg-info/top_level.txt +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/setup.cfg +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/__init__.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/ai_review.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/config.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/context_extractor.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/formatter.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/git_utils.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/llm_client.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/prompt_utils.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/prompts/config.yaml.template +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/src/prompts/review_prompt.md.template +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/tests/test_ai_review.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/tests/test_config.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/tests/test_context_extractor.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/tests/test_formatter.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/tests/test_git_utils.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/tests/test_llm_client.py +0 -0
- {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/tests/test_prompt_utils.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "code-review-ai-cli"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.2"
|
|
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"
|
|
@@ -290,13 +290,16 @@ class TFSClient:
|
|
|
290
290
|
|
|
291
291
|
files = []
|
|
292
292
|
for change in changes.get("changeEntries", []):
|
|
293
|
-
item
|
|
293
|
+
# `item` can be null for deleted files; use `or {}` as a safe fallback.
|
|
294
|
+
item = change.get("item") or {}
|
|
294
295
|
if item.get("isFolder"):
|
|
295
296
|
continue
|
|
297
|
+
# `path` can be null for deleted files; fall back to `originalPath`.
|
|
298
|
+
path = item.get("path") or change.get("originalPath") or ""
|
|
296
299
|
files.append({
|
|
297
|
-
"path":
|
|
300
|
+
"path": path,
|
|
298
301
|
"change_type": change.get("changeType", "unknown"),
|
|
299
|
-
"original_path": change.get("originalPath"
|
|
302
|
+
"original_path": change.get("originalPath") or path,
|
|
300
303
|
})
|
|
301
304
|
return files
|
|
302
305
|
|
|
@@ -339,8 +342,7 @@ class TFSClient:
|
|
|
339
342
|
Diff as text, ready to be passed to the LLM client.
|
|
340
343
|
|
|
341
344
|
Raises:
|
|
342
|
-
TFSError: If the PR has no iterations
|
|
343
|
-
after filtering.
|
|
345
|
+
TFSError: If the PR has no iterations.
|
|
344
346
|
"""
|
|
345
347
|
# Get PR details
|
|
346
348
|
path = f"git/repositories/{repository}/pullrequests/{pr_id}"
|
|
@@ -377,14 +379,22 @@ class TFSClient:
|
|
|
377
379
|
# Build diff from the changes
|
|
378
380
|
diff_parts = []
|
|
379
381
|
for change in changes.get("changeEntries", []):
|
|
380
|
-
item
|
|
382
|
+
# `item` can be null for deleted files; use `or {}` as a safe fallback.
|
|
383
|
+
item = change.get("item") or {}
|
|
381
384
|
change_type = change.get("changeType", "unknown")
|
|
382
|
-
file_path = item.get("path", "unknown")
|
|
383
|
-
original_path = change.get("originalPath") or file_path
|
|
384
385
|
|
|
385
386
|
if item.get("isFolder"):
|
|
386
387
|
continue
|
|
387
388
|
|
|
389
|
+
# Deleted files are skipped from the LLM diff.
|
|
390
|
+
# They are still reported in the terminal summary via _get_pr_changed_files.
|
|
391
|
+
if change_type == "delete":
|
|
392
|
+
continue
|
|
393
|
+
|
|
394
|
+
# `path` can be null; fall back to `originalPath` (safe for add/edit/rename).
|
|
395
|
+
file_path = item.get("path") or change.get("originalPath") or "unknown"
|
|
396
|
+
original_path = change.get("originalPath") or file_path
|
|
397
|
+
|
|
388
398
|
# --- Early-exit filters (no HTTP request made for skipped files) ---
|
|
389
399
|
# Normalise once: strip leading slash, lower-case, forward slashes.
|
|
390
400
|
norm_path = file_path.replace("\\", "/").lstrip("/").lower()
|
|
@@ -425,7 +435,7 @@ class TFSClient:
|
|
|
425
435
|
diff_parts.append("")
|
|
426
436
|
|
|
427
437
|
if not diff_parts:
|
|
428
|
-
|
|
438
|
+
return ""
|
|
429
439
|
|
|
430
440
|
return "\n".join(diff_parts)
|
|
431
441
|
|
|
@@ -500,7 +510,8 @@ class TFSClient:
|
|
|
500
510
|
file_path: Server path of the new version of the file.
|
|
501
511
|
original_path: Server path of the old version (differs on renames).
|
|
502
512
|
change_type: TFS change type string (``"edit"``, ``"add"``,
|
|
503
|
-
``"rename"
|
|
513
|
+
``"rename"``). ``"delete"`` entries are filtered out
|
|
514
|
+
by the caller before this method is invoked.
|
|
504
515
|
source_branch: Source branch ref (feature branch — new version).
|
|
505
516
|
target_branch: Target branch ref (base branch — old version).
|
|
506
517
|
|
|
@@ -326,8 +326,8 @@ def test_get_pull_request_diff_raises_for_missing_iterations_or_changes(mocker)
|
|
|
326
326
|
{"changeEntries": []},
|
|
327
327
|
],
|
|
328
328
|
)
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
result = client.get_pull_request_diff("repo-a", 1)
|
|
330
|
+
assert result == ""
|
|
331
331
|
|
|
332
332
|
|
|
333
333
|
def test_get_pull_request_diff_filters_excluded_paths(mocker) -> None:
|
|
@@ -392,8 +392,8 @@ def test_get_pull_request_diff_filters_extensions(mocker) -> None:
|
|
|
392
392
|
)
|
|
393
393
|
|
|
394
394
|
|
|
395
|
-
def
|
|
396
|
-
"""It should
|
|
395
|
+
def test_get_pull_request_diff_returns_empty_if_all_filtered(mocker) -> None:
|
|
396
|
+
"""It should return an empty string (not raise) if all files are filtered out."""
|
|
397
397
|
client = TFSClient(make_tfs_config())
|
|
398
398
|
mocker.patch(
|
|
399
399
|
"src.tfs_client.TFSClient._get",
|
|
@@ -407,10 +407,117 @@ def test_get_pull_request_diff_raises_if_all_filtered(mocker) -> None:
|
|
|
407
407
|
)
|
|
408
408
|
mocker.patch("src.tfs_client.TFSClient._build_unified_diff_part", return_value=["UNIFIED"])
|
|
409
409
|
|
|
410
|
-
# Exclude IoT. All files are filtered
|
|
411
|
-
|
|
412
|
-
|
|
410
|
+
# Exclude IoT. All files are filtered — returns empty string instead of raising.
|
|
411
|
+
result = client.get_pull_request_diff("repo-a", 1, excluded_paths=["IoT"])
|
|
412
|
+
assert result == ""
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def test_get_pr_changed_files_handles_null_item(mocker) -> None:
|
|
416
|
+
"""It should not crash and use originalPath as fallback when item is null (deleted files)."""
|
|
417
|
+
client = TFSClient(make_tfs_config())
|
|
418
|
+
mocker.patch(
|
|
419
|
+
"src.tfs_client.TFSClient._get",
|
|
420
|
+
side_effect=[
|
|
421
|
+
{"value": [{"id": 1}]},
|
|
422
|
+
{"changeEntries": [
|
|
423
|
+
# Deleted file: API returns item=null and provides originalPath instead.
|
|
424
|
+
{"item": None, "changeType": "delete", "originalPath": "/src/deleted.py"},
|
|
425
|
+
# Normal file alongside the delete.
|
|
426
|
+
{"item": {"path": "/src/kept.py"}, "changeType": "edit", "originalPath": ""},
|
|
427
|
+
]},
|
|
428
|
+
],
|
|
429
|
+
)
|
|
430
|
+
result = client._get_pr_changed_files("repo-a", 1)
|
|
431
|
+
|
|
432
|
+
# Both entries must be surfaced to the terminal summary without crashing.
|
|
433
|
+
assert len(result) == 2
|
|
434
|
+
deleted = next(r for r in result if r["change_type"] == "delete")
|
|
435
|
+
assert deleted["path"] == "/src/deleted.py"
|
|
436
|
+
assert deleted["original_path"] == "/src/deleted.py"
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def test_get_pull_request_diff_skips_deleted_files(mocker) -> None:
|
|
440
|
+
"""Deleted files must be excluded from the LLM diff without raising an error."""
|
|
441
|
+
client = TFSClient(make_tfs_config())
|
|
442
|
+
mocker.patch(
|
|
443
|
+
"src.tfs_client.TFSClient._get",
|
|
444
|
+
side_effect=[
|
|
445
|
+
{"sourceRefName": "refs/heads/feature", "targetRefName": "refs/heads/main"},
|
|
446
|
+
{"value": [{"id": 1}]},
|
|
447
|
+
{"changeEntries": [
|
|
448
|
+
# Deleted file — item is null, API provides only originalPath.
|
|
449
|
+
{"item": None, "changeType": "delete", "originalPath": "/src/removed.py"},
|
|
450
|
+
# Edited file that should still appear in the diff.
|
|
451
|
+
{"item": {"path": "/src/app.py"}, "changeType": "edit", "originalPath": "/src/app.py"},
|
|
452
|
+
]},
|
|
453
|
+
],
|
|
454
|
+
)
|
|
455
|
+
unified = mocker.patch(
|
|
456
|
+
"src.tfs_client.TFSClient._build_unified_diff_part", return_value=["UNIFIED"]
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
result = client.get_pull_request_diff("repo-a", 1)
|
|
460
|
+
|
|
461
|
+
# The diff builder must only be called for the edited file, not the deleted one.
|
|
462
|
+
unified.assert_called_once_with(
|
|
463
|
+
repository="repo-a",
|
|
464
|
+
file_path="/src/app.py",
|
|
465
|
+
original_path="/src/app.py",
|
|
466
|
+
change_type="edit",
|
|
467
|
+
source_branch="refs/heads/feature",
|
|
468
|
+
target_branch="refs/heads/main",
|
|
469
|
+
)
|
|
470
|
+
assert "UNIFIED" in result
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def test_get_pull_request_diff_handles_null_item_in_non_delete(mocker) -> None:
|
|
474
|
+
"""A null item for a non-delete change must not crash; originalPath is used as path fallback."""
|
|
475
|
+
client = TFSClient(make_tfs_config())
|
|
476
|
+
mocker.patch(
|
|
477
|
+
"src.tfs_client.TFSClient._get",
|
|
478
|
+
side_effect=[
|
|
479
|
+
{"sourceRefName": "refs/heads/feature", "targetRefName": "refs/heads/main"},
|
|
480
|
+
{"value": [{"id": 1}]},
|
|
481
|
+
{"changeEntries": [
|
|
482
|
+
# Unexpected null item for an edit — defensive case.
|
|
483
|
+
{"item": None, "changeType": "edit", "originalPath": "/src/fallback.py"},
|
|
484
|
+
]},
|
|
485
|
+
],
|
|
486
|
+
)
|
|
487
|
+
unified = mocker.patch(
|
|
488
|
+
"src.tfs_client.TFSClient._build_unified_diff_part", return_value=["UNIFIED"]
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
result = client.get_pull_request_diff("repo-a", 1)
|
|
492
|
+
|
|
493
|
+
unified.assert_called_once_with(
|
|
494
|
+
repository="repo-a",
|
|
495
|
+
file_path="/src/fallback.py",
|
|
496
|
+
original_path="/src/fallback.py",
|
|
497
|
+
change_type="edit",
|
|
498
|
+
source_branch="refs/heads/feature",
|
|
499
|
+
target_branch="refs/heads/main",
|
|
500
|
+
)
|
|
501
|
+
assert "UNIFIED" in result
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def test_get_pull_request_diff_returns_empty_when_only_deletes_present(mocker) -> None:
|
|
505
|
+
"""It should return an empty string (not raise) when the PR contains only deleted files."""
|
|
506
|
+
client = TFSClient(make_tfs_config())
|
|
507
|
+
mocker.patch(
|
|
508
|
+
"src.tfs_client.TFSClient._get",
|
|
509
|
+
side_effect=[
|
|
510
|
+
{"sourceRefName": "refs/heads/feature", "targetRefName": "refs/heads/main"},
|
|
511
|
+
{"value": [{"id": 1}]},
|
|
512
|
+
{"changeEntries": [
|
|
513
|
+
{"item": None, "changeType": "delete", "originalPath": "/src/a.py"},
|
|
514
|
+
{"item": None, "changeType": "delete", "originalPath": "/src/b.py"},
|
|
515
|
+
]},
|
|
516
|
+
],
|
|
517
|
+
)
|
|
413
518
|
|
|
519
|
+
result = client.get_pull_request_diff("repo-a", 1)
|
|
520
|
+
assert result == ""
|
|
414
521
|
|
|
415
522
|
|
|
416
523
|
def test_build_diff_parts_and_file_content(mocker) -> None:
|
|
@@ -488,7 +595,12 @@ def test_build_unified_diff_part_context_block_present_on_add(mocker) -> None:
|
|
|
488
595
|
|
|
489
596
|
|
|
490
597
|
def test_build_unified_diff_part_no_context_block_on_delete(mocker) -> None:
|
|
491
|
-
"""_build_unified_diff_part must NOT append any context block when change_type is delete.
|
|
598
|
+
"""_build_unified_diff_part must NOT append any context block when change_type is delete.
|
|
599
|
+
|
|
600
|
+
Note: in production this code path is no longer reached via get_pull_request_diff —
|
|
601
|
+
deleted entries are filtered out before _build_unified_diff_part is ever called.
|
|
602
|
+
This test is kept as a defensive contract on the method's own behaviour.
|
|
603
|
+
"""
|
|
492
604
|
client = TFSClient(make_tfs_config())
|
|
493
605
|
mocker.patch(
|
|
494
606
|
"src.tfs_client.TFSClient._get_file_content",
|
|
File without changes
|
{code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/code_review_ai_cli.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/code_review_ai_cli.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/code_review_ai_cli.egg-info/requires.txt
RENAMED
|
File without changes
|
{code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.2}/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
|
|
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
|
|
File without changes
|