code-review-ai-cli 2.2.0__tar.gz → 2.2.1__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 (29) hide show
  1. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/PKG-INFO +1 -1
  2. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/code_review_ai_cli.egg-info/PKG-INFO +1 -1
  3. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/pyproject.toml +1 -1
  4. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/tfs_client.py +19 -7
  5. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/tests/test_tfs_client.py +113 -1
  6. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/README.md +0 -0
  7. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/code_review_ai_cli.egg-info/SOURCES.txt +0 -0
  8. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/code_review_ai_cli.egg-info/dependency_links.txt +0 -0
  9. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/code_review_ai_cli.egg-info/entry_points.txt +0 -0
  10. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/code_review_ai_cli.egg-info/requires.txt +0 -0
  11. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/code_review_ai_cli.egg-info/top_level.txt +0 -0
  12. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/setup.cfg +0 -0
  13. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/__init__.py +0 -0
  14. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/ai_review.py +0 -0
  15. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/config.py +0 -0
  16. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/context_extractor.py +0 -0
  17. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/formatter.py +0 -0
  18. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/git_utils.py +0 -0
  19. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/llm_client.py +0 -0
  20. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/prompt_utils.py +0 -0
  21. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/prompts/config.yaml.template +0 -0
  22. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/src/prompts/review_prompt.md.template +0 -0
  23. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/tests/test_ai_review.py +0 -0
  24. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/tests/test_config.py +0 -0
  25. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/tests/test_context_extractor.py +0 -0
  26. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/tests/test_formatter.py +0 -0
  27. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/tests/test_git_utils.py +0 -0
  28. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/tests/test_llm_client.py +0 -0
  29. {code_review_ai_cli-2.2.0 → code_review_ai_cli-2.2.1}/tests/test_prompt_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-review-ai-cli
3
- Version: 2.2.0
3
+ Version: 2.2.1
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: code-review-ai-cli
3
- Version: 2.2.0
3
+ Version: 2.2.1
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
@@ -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.0"
7
+ version = "2.2.1"
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 = change.get("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": item.get("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
 
@@ -377,14 +380,22 @@ class TFSClient:
377
380
  # Build diff from the changes
378
381
  diff_parts = []
379
382
  for change in changes.get("changeEntries", []):
380
- item = change.get("item", {})
383
+ # `item` can be null for deleted files; use `or {}` as a safe fallback.
384
+ item = change.get("item") or {}
381
385
  change_type = change.get("changeType", "unknown")
382
- file_path = item.get("path", "unknown")
383
- original_path = change.get("originalPath") or file_path
384
386
 
385
387
  if item.get("isFolder"):
386
388
  continue
387
389
 
390
+ # Deleted files are skipped from the LLM diff.
391
+ # They are still reported in the terminal summary via _get_pr_changed_files.
392
+ if change_type == "delete":
393
+ continue
394
+
395
+ # `path` can be null; fall back to `originalPath` (safe for add/edit/rename).
396
+ file_path = item.get("path") or change.get("originalPath") or "unknown"
397
+ original_path = change.get("originalPath") or file_path
398
+
388
399
  # --- Early-exit filters (no HTTP request made for skipped files) ---
389
400
  # Normalise once: strip leading slash, lower-case, forward slashes.
390
401
  norm_path = file_path.replace("\\", "/").lstrip("/").lower()
@@ -500,7 +511,8 @@ class TFSClient:
500
511
  file_path: Server path of the new version of the file.
501
512
  original_path: Server path of the old version (differs on renames).
502
513
  change_type: TFS change type string (``"edit"``, ``"add"``,
503
- ``"rename"``, ``"delete"``, …).
514
+ ``"rename"``). ``"delete"`` entries are filtered out
515
+ by the caller before this method is invoked.
504
516
  source_branch: Source branch ref (feature branch — new version).
505
517
  target_branch: Target branch ref (base branch — old version).
506
518
 
@@ -412,6 +412,113 @@ def test_get_pull_request_diff_raises_if_all_filtered(mocker) -> None:
412
412
  client.get_pull_request_diff("repo-a", 1, excluded_paths=["IoT"])
413
413
 
414
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_raises_when_only_deletes_present(mocker) -> None:
505
+ """It should raise TFSError when the PR contains only deleted files (all skipped)."""
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
+ )
518
+
519
+ with pytest.raises(TFSError, match="contains no file changes after filtering"):
520
+ client.get_pull_request_diff("repo-a", 1)
521
+
415
522
 
416
523
  def test_build_diff_parts_and_file_content(mocker) -> None:
417
524
  """It should build full-code and unified diff payloads from file content."""
@@ -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",