claude-dev-env 1.26.5 → 1.27.0

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.
@@ -0,0 +1,271 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import sys
5
+ import unittest.mock
6
+ from pathlib import Path
7
+ import pytest
8
+
9
+ SCRIPT_DIRECTORY = Path(__file__).resolve().parent
10
+ if str(SCRIPT_DIRECTORY) not in sys.path:
11
+ sys.path.insert(0, str(SCRIPT_DIRECTORY))
12
+
13
+ import bugteam_code_rules_gate as gate_module
14
+
15
+
16
+ def run_git_in_repository(repository_root: Path, *arguments: str) -> str:
17
+ completion = subprocess.run(
18
+ ["git", *arguments],
19
+ cwd=str(repository_root),
20
+ capture_output=True,
21
+ text=True,
22
+ encoding="utf-8",
23
+ errors="replace",
24
+ check=True,
25
+ )
26
+ return completion.stdout
27
+
28
+
29
+ def initialize_git_repository(repository_root: Path) -> None:
30
+ run_git_in_repository(repository_root, "init", "--initial-branch=main")
31
+ run_git_in_repository(repository_root, "config", "user.email", "test@example.com")
32
+ run_git_in_repository(repository_root, "config", "user.name", "Test")
33
+ run_git_in_repository(repository_root, "config", "commit.gpgsign", "false")
34
+
35
+
36
+ def commit_all_files(repository_root: Path, commit_message: str) -> None:
37
+ run_git_in_repository(repository_root, "add", "-A")
38
+ run_git_in_repository(repository_root, "commit", "-m", commit_message)
39
+
40
+
41
+ def write_file(file_path: Path, content: str) -> None:
42
+ file_path.parent.mkdir(parents=True, exist_ok=True)
43
+ file_path.write_text(content, encoding="utf-8")
44
+
45
+
46
+ def stage_file(repository_root: Path, relative_path: str) -> None:
47
+ run_git_in_repository(repository_root, "add", "--", relative_path)
48
+
49
+
50
+ @pytest.fixture()
51
+ def temporary_git_repository(tmp_path: Path) -> Path:
52
+ repository_root = tmp_path / "repository_under_test"
53
+ repository_root.mkdir()
54
+ initialize_git_repository(repository_root)
55
+ return repository_root
56
+
57
+
58
+ def test_paths_from_git_staged_returns_staged_files(
59
+ temporary_git_repository: Path,
60
+ ) -> None:
61
+ write_file(temporary_git_repository / "committed_file.py", "one = 1\n")
62
+ commit_all_files(temporary_git_repository, "initial")
63
+ write_file(temporary_git_repository / "newly_staged_file.py", "two = 2\n")
64
+ write_file(temporary_git_repository / "unstaged_file.py", "three = 3\n")
65
+ stage_file(temporary_git_repository, "newly_staged_file.py")
66
+
67
+ staged_paths = gate_module.paths_from_git_staged(temporary_git_repository)
68
+
69
+ staged_names = {path.name for path in staged_paths}
70
+ assert "newly_staged_file.py" in staged_names
71
+ assert "unstaged_file.py" not in staged_names
72
+ assert "committed_file.py" not in staged_names
73
+
74
+
75
+ def test_added_lines_for_staged_file_reports_new_lines(
76
+ temporary_git_repository: Path,
77
+ ) -> None:
78
+ write_file(temporary_git_repository / "target.py", "first = 1\nsecond = 2\n")
79
+ commit_all_files(temporary_git_repository, "baseline")
80
+ write_file(
81
+ temporary_git_repository / "target.py",
82
+ "first = 1\nsecond = 2\nthird = 3\nfourth = 4\n",
83
+ )
84
+ stage_file(temporary_git_repository, "target.py")
85
+
86
+ added_line_numbers = gate_module.added_lines_for_staged_file(
87
+ temporary_git_repository,
88
+ "target.py",
89
+ )
90
+
91
+ assert 3 in added_line_numbers
92
+ assert 4 in added_line_numbers
93
+ assert 1 not in added_line_numbers
94
+ assert 2 not in added_line_numbers
95
+
96
+
97
+ def test_added_lines_for_staged_file_treats_new_file_as_fully_added(
98
+ temporary_git_repository: Path,
99
+ ) -> None:
100
+ write_file(temporary_git_repository / "existing.py", "ignored = 0\n")
101
+ commit_all_files(temporary_git_repository, "baseline")
102
+ write_file(
103
+ temporary_git_repository / "brand_new.py",
104
+ "alpha = 1\nbeta = 2\ngamma = 3\n",
105
+ )
106
+ stage_file(temporary_git_repository, "brand_new.py")
107
+
108
+ added_line_numbers = gate_module.added_lines_for_staged_file(
109
+ temporary_git_repository,
110
+ "brand_new.py",
111
+ )
112
+
113
+ assert added_line_numbers == {1, 2, 3}
114
+
115
+
116
+ def test_paths_from_git_staged_uses_null_delimiter(
117
+ temporary_git_repository: Path,
118
+ ) -> None:
119
+ write_file(temporary_git_repository / "first.py", "a = 1\n")
120
+ write_file(temporary_git_repository / "second.py", "b = 2\n")
121
+ commit_all_files(temporary_git_repository, "baseline")
122
+ write_file(temporary_git_repository / "first.py", "a = 10\n")
123
+ write_file(temporary_git_repository / "second.py", "b = 20\n")
124
+ stage_file(temporary_git_repository, "first.py")
125
+ stage_file(temporary_git_repository, "second.py")
126
+
127
+ staged_paths = gate_module.paths_from_git_staged(temporary_git_repository)
128
+
129
+ staged_names = {path.name for path in staged_paths}
130
+ assert staged_names == {"first.py", "second.py"}
131
+
132
+
133
+ def test_paths_from_git_staged_warns_and_skips_non_utf8_filename(
134
+ tmp_path: Path,
135
+ capsys: pytest.CaptureFixture[str],
136
+ ) -> None:
137
+ non_utf8_raw = b"valid.py\x00\xff\xfe_bad.py\x00"
138
+ mock_completed = unittest.mock.MagicMock()
139
+ mock_completed.returncode = 0
140
+ mock_completed.stdout = non_utf8_raw
141
+
142
+ with unittest.mock.patch("subprocess.run", return_value=mock_completed):
143
+ result_paths = gate_module.paths_from_git_staged(tmp_path)
144
+
145
+ captured = capsys.readouterr()
146
+ assert "non-UTF-8" in captured.err
147
+ assert len(result_paths) == 1
148
+ assert result_paths[0].name == "valid.py"
149
+
150
+
151
+ def test_staged_added_lines_by_file_maps_every_staged_code_file(
152
+ temporary_git_repository: Path,
153
+ ) -> None:
154
+ write_file(temporary_git_repository / "already_committed.py", "zero = 0\n")
155
+ commit_all_files(temporary_git_repository, "initial")
156
+ write_file(
157
+ temporary_git_repository / "already_committed.py",
158
+ "zero = 0\nappended = 1\n",
159
+ )
160
+ write_file(temporary_git_repository / "added_file.py", "only = 1\n")
161
+ stage_file(temporary_git_repository, "already_committed.py")
162
+ stage_file(temporary_git_repository, "added_file.py")
163
+
164
+ staged_paths = gate_module.paths_from_git_staged(temporary_git_repository)
165
+ added_lines_map = gate_module.added_lines_by_file_staged(
166
+ temporary_git_repository,
167
+ staged_paths,
168
+ )
169
+
170
+ resolved_repository_root = temporary_git_repository.resolve()
171
+ assert added_lines_map[resolved_repository_root / "already_committed.py"] == {2}
172
+ assert added_lines_map[resolved_repository_root / "added_file.py"] == {1}
173
+
174
+
175
+ def test_main_staged_mode_blocks_when_staged_lines_introduce_violations(
176
+ temporary_git_repository: Path,
177
+ monkeypatch: pytest.MonkeyPatch,
178
+ ) -> None:
179
+ write_file(temporary_git_repository / "module.py", "first_value = 1\n")
180
+ commit_all_files(temporary_git_repository, "initial")
181
+ staged_content_with_banned_identifier = (
182
+ "first_value = 1\n"
183
+ "def compute_total(operand):\n"
184
+ " result = operand + 1\n"
185
+ " return result\n"
186
+ )
187
+ write_file(
188
+ temporary_git_repository / "module.py",
189
+ staged_content_with_banned_identifier,
190
+ )
191
+ stage_file(temporary_git_repository, "module.py")
192
+
193
+ monkeypatch.chdir(temporary_git_repository)
194
+ exit_code = gate_module.main(["--staged"])
195
+
196
+ assert exit_code == 1
197
+
198
+
199
+ def test_main_staged_mode_passes_when_no_staged_violations(
200
+ temporary_git_repository: Path,
201
+ monkeypatch: pytest.MonkeyPatch,
202
+ ) -> None:
203
+ write_file(temporary_git_repository / "module.py", "first_value = 1\n")
204
+ commit_all_files(temporary_git_repository, "initial")
205
+ write_file(
206
+ temporary_git_repository / "module.py", "first_value = 1\nsecond_value = 2\n"
207
+ )
208
+ stage_file(temporary_git_repository, "module.py")
209
+
210
+ monkeypatch.chdir(temporary_git_repository)
211
+ exit_code = gate_module.main(["--staged"])
212
+
213
+ assert exit_code == 0
214
+
215
+
216
+ def test_main_staged_mode_exits_zero_when_nothing_staged(
217
+ temporary_git_repository: Path,
218
+ monkeypatch: pytest.MonkeyPatch,
219
+ ) -> None:
220
+ write_file(temporary_git_repository / "module.py", "first_value = 1\n")
221
+ commit_all_files(temporary_git_repository, "initial")
222
+
223
+ monkeypatch.chdir(temporary_git_repository)
224
+ exit_code = gate_module.main(["--staged"])
225
+
226
+ assert exit_code == 0
227
+
228
+
229
+ def test_added_lines_for_staged_file_returns_empty_for_modified_file_with_no_additions(
230
+ temporary_git_repository: Path,
231
+ ) -> None:
232
+ write_file(
233
+ temporary_git_repository / "existing.py",
234
+ "alpha = 1\nbeta = 2\ngamma = 3\n",
235
+ )
236
+ commit_all_files(temporary_git_repository, "baseline")
237
+ write_file(temporary_git_repository / "existing.py", "alpha = 1\nbeta = 2\n")
238
+ stage_file(temporary_git_repository, "existing.py")
239
+
240
+ added_line_numbers = gate_module.added_lines_for_staged_file(
241
+ temporary_git_repository,
242
+ "existing.py",
243
+ )
244
+
245
+ assert added_line_numbers == set()
246
+
247
+
248
+ def test_is_file_absent_in_index_head_does_not_exist_in_module() -> None:
249
+ assert not hasattr(gate_module, "is_file_absent_in_index_head")
250
+
251
+
252
+ def test_added_lines_for_staged_file_returns_parsed_result_when_diff_is_non_empty_even_if_parse_returns_empty(
253
+ temporary_git_repository: Path,
254
+ monkeypatch: pytest.MonkeyPatch,
255
+ ) -> None:
256
+ write_file(
257
+ temporary_git_repository / "sample.py",
258
+ "alpha = 1\nbeta = 2\n",
259
+ )
260
+ commit_all_files(temporary_git_repository, "baseline")
261
+ write_file(temporary_git_repository / "sample.py", "alpha = 1\nbeta = 2\ngamma = 3\n")
262
+ stage_file(temporary_git_repository, "sample.py")
263
+
264
+ monkeypatch.setattr(gate_module, "parse_added_line_numbers", lambda _text: set())
265
+
266
+ added_line_numbers = gate_module.added_lines_for_staged_file(
267
+ temporary_git_repository,
268
+ "sample.py",
269
+ )
270
+
271
+ assert added_line_numbers == set()