Skip to content

Commit 5658ed3

Browse files
authored
Merge pull request #71 from codesensei-tushar/fix/file-pattern-changed-files
fix: implement _get_changed_files for FilePatternCondition
2 parents 7ab4368 + 92c85c2 commit 5658ed3

3 files changed

Lines changed: 138 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88

99
### Fixed
1010

11+
- **`FilePatternCondition._get_changed_files` implementation** -- Replaced
12+
stub that always returned `[]` with a working implementation that extracts
13+
file paths from enriched PR data (`changed_files` list of dicts or plain
14+
strings) and push event commits (`added`/`modified`/`removed` arrays with
15+
deduplication). Added unit tests covering all extraction paths.
16+
17+
## [2026-04-12] -- PR #69
18+
19+
### Fixed
20+
1121
- **Blocking sleep in LLM condition** -- Replaced `time.sleep()` with
1222
`await asyncio.sleep()` in `LLMAssisted` retry backoff to avoid
1323
freezing the event loop during LLM retries.
1424

25+
## [2026-04-08] -- PR #66
26+
1527
### Added
1628

1729
- **AI-powered reviewer recommendation** -- `/reviewers` slash command suggests

src/rules/conditions/filesystem.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -116,16 +116,33 @@ async def validate(self, parameters: dict[str, Any], event: dict[str, Any]) -> b
116116
return len(matching_files) > 0
117117

118118
def _get_changed_files(self, event: dict[str, Any]) -> list[str]:
119-
"""Extract the list of changed files from the event."""
120-
event_type = event.get("event_type", "")
121-
if event_type == "pull_request":
122-
# TODO: Pull request—fetch changed files via GitHub API. Placeholder for now.
123-
return []
124-
elif event_type == "push":
125-
# Push event—files in commits, not implemented.
126-
return []
127-
else:
128-
return []
119+
"""Extract changed file paths from enriched PR data or push commits."""
120+
changed_files = event.get("changed_files", [])
121+
if isinstance(changed_files, list) and changed_files:
122+
extracted: list[str] = []
123+
for item in changed_files:
124+
path = item.get("filename") if isinstance(item, dict) else item
125+
if isinstance(path, str) and path:
126+
extracted.append(path)
127+
if extracted:
128+
return extracted
129+
130+
commits = event.get("commits", [])
131+
if isinstance(commits, list) and commits:
132+
seen: set[str] = set()
133+
for commit in commits:
134+
if not isinstance(commit, dict):
135+
continue
136+
for key in ("added", "modified", "removed"):
137+
paths = commit.get(key, [])
138+
if not isinstance(paths, list):
139+
continue
140+
for path in paths:
141+
if isinstance(path, str) and path:
142+
seen.add(path)
143+
return sorted(seen)
144+
145+
return []
129146

130147
@staticmethod
131148
def _glob_to_regex(glob_pattern: str) -> str:

tests/unit/rules/conditions/test_filesystem.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Tests for FilePatternCondition, MaxFileSizeCondition, and MaxPrLocCondition classes.
44
"""
55

6+
from typing import Any
67
from unittest.mock import patch
78

89
import pytest
@@ -96,6 +97,104 @@ def test_glob_to_regex_conversion(self) -> None:
9697
assert FilePatternCondition._glob_to_regex("src/*.js") == "^src/.*\\.js$"
9798
assert FilePatternCondition._glob_to_regex("file?.txt") == "^file.\\.txt$"
9899

100+
def test_get_changed_files_from_pr_enriched_data(self) -> None:
101+
"""Test extracting files from enriched PR changed_files (list of dicts)."""
102+
condition = FilePatternCondition()
103+
event = {
104+
"changed_files": [
105+
{"filename": "src/main.py", "status": "modified", "additions": 10, "deletions": 2},
106+
{"filename": "tests/test_main.py", "status": "added", "additions": 30, "deletions": 0},
107+
]
108+
}
109+
result = condition._get_changed_files(event)
110+
assert result == ["src/main.py", "tests/test_main.py"]
111+
112+
def test_get_changed_files_from_pr_plain_strings(self) -> None:
113+
"""Test extracting files when changed_files contains plain strings."""
114+
condition = FilePatternCondition()
115+
event = {"changed_files": ["src/main.py", "README.md"]}
116+
result = condition._get_changed_files(event)
117+
assert result == ["src/main.py", "README.md"]
118+
119+
def test_get_changed_files_from_push_commits(self) -> None:
120+
"""Test extracting files from push event commit arrays."""
121+
condition = FilePatternCondition()
122+
event = {
123+
"commits": [
124+
{"added": ["new_file.py"], "modified": ["src/main.py"], "removed": []},
125+
{"added": [], "modified": ["src/main.py"], "removed": ["old.py"]},
126+
]
127+
}
128+
result = condition._get_changed_files(event)
129+
assert result == ["new_file.py", "old.py", "src/main.py"]
130+
131+
def test_get_changed_files_empty_event(self) -> None:
132+
"""Test that an empty event returns no files."""
133+
condition = FilePatternCondition()
134+
assert condition._get_changed_files({}) == []
135+
136+
def test_get_changed_files_with_malformed_payload(self) -> None:
137+
"""Test that malformed payload entries are filtered out without raising."""
138+
condition = FilePatternCondition()
139+
140+
# changed_files with mixed valid/invalid entries
141+
event_cf: dict[str, Any] = {
142+
"changed_files": [
143+
{"filename": "valid.py", "status": "modified"},
144+
{"status": "added"}, # missing "filename"
145+
None, # type: ignore[list-item]
146+
42, # type: ignore[list-item]
147+
"", # empty string
148+
{"filename": ""}, # empty filename
149+
"also_valid.txt",
150+
]
151+
}
152+
result = condition._get_changed_files(event_cf)
153+
assert result == ["valid.py", "also_valid.txt"]
154+
155+
# commits with non-dict entries and non-list/non-string values
156+
event_commits: dict[str, Any] = {
157+
"commits": [
158+
{"added": ["good.py"], "modified": "not_a_list", "removed": [42, None, "removed.py"]},
159+
"not_a_dict", # type: ignore[list-item]
160+
{"added": [None, "", "another.py"], "modified": [], "removed": []},
161+
]
162+
}
163+
result = condition._get_changed_files(event_commits)
164+
assert result == ["another.py", "good.py", "removed.py"]
165+
166+
@pytest.mark.asyncio
167+
async def test_evaluate_with_real_pr_event(self) -> None:
168+
"""Test full evaluate flow with enriched PR data (no mocking)."""
169+
condition = FilePatternCondition()
170+
context = {
171+
"parameters": {"pattern": "*.py", "condition_type": "files_match_pattern"},
172+
"event": {
173+
"changed_files": [
174+
{"filename": "src/app.py", "status": "modified", "additions": 5, "deletions": 1},
175+
{"filename": "docs/readme.md", "status": "modified", "additions": 2, "deletions": 0},
176+
]
177+
},
178+
}
179+
violations = await condition.evaluate(context)
180+
assert len(violations) == 0
181+
182+
@pytest.mark.asyncio
183+
async def test_evaluate_with_real_push_event(self) -> None:
184+
"""Test full evaluate flow with push commit data (no mocking)."""
185+
condition = FilePatternCondition()
186+
context = {
187+
"parameters": {"pattern": "*.yaml", "condition_type": "files_not_match_pattern"},
188+
"event": {
189+
"commits": [
190+
{"added": ["config/app.yaml"], "modified": [], "removed": []},
191+
]
192+
},
193+
}
194+
violations = await condition.evaluate(context)
195+
assert len(violations) == 1
196+
assert "forbidden pattern" in violations[0].message
197+
99198

100199
class TestMaxFileSizeCondition:
101200
"""Tests for MaxFileSizeCondition class."""

0 commit comments

Comments
 (0)