claude-dev-env 1.50.0 → 1.50.2
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.
- package/hooks/blocking/_gh_body_arg_utils.py +67 -11
- package/hooks/blocking/_md_to_html_blocker_test_support.py +65 -0
- package/hooks/blocking/code_rules_annotations_length.py +167 -0
- package/hooks/blocking/code_rules_banned_identifiers.py +385 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +350 -0
- package/hooks/blocking/code_rules_comments.py +337 -0
- package/hooks/blocking/code_rules_constants_config.py +252 -0
- package/hooks/blocking/code_rules_docstrings.py +308 -0
- package/hooks/blocking/code_rules_enforcer.py +98 -5765
- package/hooks/blocking/code_rules_imports_logging.py +276 -0
- package/hooks/blocking/code_rules_magic_values.py +180 -0
- package/hooks/blocking/code_rules_mock_completeness.py +295 -0
- package/hooks/blocking/code_rules_naming_collection.py +264 -0
- package/hooks/blocking/code_rules_optional_params.py +288 -0
- package/hooks/blocking/code_rules_paths_syspath.py +186 -0
- package/hooks/blocking/code_rules_probe_chains.py +305 -0
- package/hooks/blocking/code_rules_probe_detection.py +257 -0
- package/hooks/blocking/code_rules_probe_recording.py +225 -0
- package/hooks/blocking/code_rules_scope_binding.py +151 -0
- package/hooks/blocking/code_rules_shared.py +301 -0
- package/hooks/blocking/code_rules_string_magic.py +207 -0
- package/hooks/blocking/code_rules_test_assertions.py +226 -0
- package/hooks/blocking/code_rules_test_branching_except.py +181 -0
- package/hooks/blocking/code_rules_test_isolation.py +341 -0
- package/hooks/blocking/code_rules_type_escape.py +341 -0
- package/hooks/blocking/code_rules_typeddict_stub.py +305 -0
- package/hooks/blocking/code_rules_unused_imports.py +256 -0
- package/hooks/blocking/conftest.py +30 -0
- package/hooks/blocking/pr_description_body_audit.py +148 -0
- package/hooks/blocking/pr_description_command_parser.py +233 -0
- package/hooks/blocking/pr_description_enforcer.py +36 -825
- package/hooks/blocking/pr_description_pr_number.py +153 -0
- package/hooks/blocking/pr_description_readability.py +366 -0
- package/hooks/blocking/tdd_enforcer.py +31 -0
- package/hooks/blocking/test_code_rules_constants_config.py +26 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +5 -2
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -5
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +21 -15
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +20 -16
- package/hooks/blocking/test_code_rules_enforcer_exempt_marker_chained.py +4 -2
- package/hooks/blocking/test_code_rules_enforcer_function_length.py +154 -18
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +1 -2
- package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +22 -12
- package/hooks/blocking/test_code_rules_enforcer_split_annotations_length.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_split_banned.py +170 -0
- package/hooks/blocking/test_code_rules_enforcer_split_comments.py +60 -0
- package/hooks/blocking/test_code_rules_enforcer_split_config_path.py +52 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +236 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +296 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +238 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_1.py +271 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_2.py +283 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_3.py +268 -0
- package/hooks/blocking/test_code_rules_enforcer_split_isolation_4.py +85 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_1.py +303 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mocks_2.py +111 -0
- package/hooks/blocking/test_code_rules_enforcer_split_mustcheck.py +87 -0
- package/hooks/blocking/test_code_rules_enforcer_split_naming.py +107 -0
- package/hooks/blocking/test_code_rules_enforcer_split_optional_params.py +325 -0
- package/hooks/blocking/test_code_rules_enforcer_split_paths_syspath.py +110 -0
- package/hooks/blocking/test_code_rules_enforcer_split_shared.py +44 -0
- package/hooks/blocking/test_code_rules_enforcer_split_string_magic.py +55 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +56 -0
- package/hooks/blocking/test_code_rules_enforcer_todo_markers.py +21 -15
- package/hooks/blocking/test_code_rules_paths_syspath.py +26 -0
- package/hooks/blocking/test_md_to_html_blocker_exemptions.py +368 -0
- package/hooks/blocking/test_md_to_html_blocker_extensions.py +157 -0
- package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +336 -0
- package/hooks/blocking/test_pr_description_enforcer.py +13 -1499
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +247 -0
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +493 -0
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +366 -0
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +159 -0
- package/hooks/blocking/test_pr_description_enforcer_readability.py +443 -0
- package/hooks/blocking/test_tdd_enforcer.py +116 -0
- package/hooks/hooks_constants/blocking_check_limits.py +3 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +8 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +7 -0
- package/hooks/hooks_constants/sys_path_insert_constants.py +1 -0
- package/package.json +1 -1
- package/hooks/blocking/test_code_rules_enforcer.py +0 -2669
- package/hooks/blocking/test_md_to_html_blocker.py +0 -810
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
"""Unit tests for pr-description-enforcer body rule enforcement via validate_pr_body."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import json
|
|
5
|
+
import pathlib
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
9
|
+
_HOOKS_ROOT = _HOOK_DIR.parent
|
|
10
|
+
if str(_HOOKS_ROOT) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(_HOOKS_ROOT))
|
|
12
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from blocking import pr_description_readability as readability_module
|
|
18
|
+
|
|
19
|
+
enforcer_spec = importlib.util.spec_from_file_location(
|
|
20
|
+
"pr_description_enforcer",
|
|
21
|
+
_HOOK_DIR / "pr_description_enforcer.py",
|
|
22
|
+
)
|
|
23
|
+
assert enforcer_spec is not None
|
|
24
|
+
assert enforcer_spec.loader is not None
|
|
25
|
+
enforcer_module = importlib.util.module_from_spec(enforcer_spec)
|
|
26
|
+
enforcer_spec.loader.exec_module(enforcer_module)
|
|
27
|
+
validate_pr_body = enforcer_module.validate_pr_body
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture(autouse=True)
|
|
31
|
+
def _isolate_readability_state(tmp_path_factory, monkeypatch):
|
|
32
|
+
"""Redirect the three readability state files to per-test temp paths for every test.
|
|
33
|
+
|
|
34
|
+
The enabled file is written with enabled=False so the readability check stays
|
|
35
|
+
off for the body-rule tests, isolating them from readability scoring and the
|
|
36
|
+
live state directory.
|
|
37
|
+
"""
|
|
38
|
+
per_test_state_dir = tmp_path_factory.mktemp("readability_state")
|
|
39
|
+
strike_path = per_test_state_dir / "strikes.json"
|
|
40
|
+
override_path = per_test_state_dir / "overrides.json"
|
|
41
|
+
enabled_path = per_test_state_dir / "enabled.json"
|
|
42
|
+
enabled_path.write_text(json.dumps({"enabled": False}))
|
|
43
|
+
monkeypatch.setattr(readability_module, "READABILITY_STATE_FILE", strike_path)
|
|
44
|
+
monkeypatch.setattr(readability_module, "READABILITY_THRESHOLD_OVERRIDE_FILE", override_path)
|
|
45
|
+
monkeypatch.setattr(readability_module, "READABILITY_ENABLED_STATE_FILE", enabled_path)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
VALID_BODY = (
|
|
49
|
+
"Allow commas in branch names so PRs whose head branch was generated from "
|
|
50
|
+
"a title or external identifier no longer fail validation before any git "
|
|
51
|
+
"operation.\n\n"
|
|
52
|
+
"Fixes #1300.\n\n"
|
|
53
|
+
"## Changes\n\n"
|
|
54
|
+
"- `src/github/operations/branch.ts`: add `,` to the whitelist regex\n"
|
|
55
|
+
"- `test/branch.test.ts`: 3 new cases covering comma-bearing branch names\n\n"
|
|
56
|
+
"## Test plan\n\n"
|
|
57
|
+
"- `bun test test/branch.test.ts`\n"
|
|
58
|
+
"- `bun run typecheck`\n"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
LEGACY_DESCRIPTION_WHY_HOW_BODY = (
|
|
62
|
+
"## Description\n\nFixes a real bug in the authentication module that affected production users.\n\n"
|
|
63
|
+
"## Why\n\nThe defect surfaced in production and customers reported repeated sign-in failures.\n\n"
|
|
64
|
+
"## How\n\nRefactored the auth module to handle edge cases correctly.\n"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _has_vague_language_violation(all_violations: list[str]) -> bool:
|
|
69
|
+
return any("Vague language" in each_violation for each_violation in all_violations)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _build_heavy_body(opening_header: str, testing_header: str) -> str:
|
|
73
|
+
intro_text = (
|
|
74
|
+
"Adds shape-aware validation across the pr-description-enforcer pipeline. "
|
|
75
|
+
"The change unifies the body audit with the Anthropic claude-code style "
|
|
76
|
+
"so heavy PRs carry both an opening header and a testing header."
|
|
77
|
+
)
|
|
78
|
+
return (
|
|
79
|
+
f"{intro_text}\n\n"
|
|
80
|
+
f"{opening_header}\n\n"
|
|
81
|
+
"The earlier flow rejected too many valid bodies on equivalence checks "
|
|
82
|
+
"across the three shape categories described in the guide. The fix "
|
|
83
|
+
"restructures the path around shape detection and surfaces the missing "
|
|
84
|
+
"category in the block message so the agent can correct it on first try.\n\n"
|
|
85
|
+
f"{testing_header}\n\n"
|
|
86
|
+
"- `pytest packages/claude-dev-env/hooks/blocking/test_pr_description_enforcer.py`\n"
|
|
87
|
+
"- Manual smoke test against the implementation PR with a sample heavy body\n"
|
|
88
|
+
"- Run the readability check across the full corpus to confirm thresholds hold\n"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_validate_passes_anthropic_standard_body() -> None:
|
|
93
|
+
assert validate_pr_body(VALID_BODY) == []
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_validate_passes_legacy_description_why_how_body() -> None:
|
|
97
|
+
"""Existing Description/Why/How bodies must still pass -- the relaxed rule only widens what's accepted."""
|
|
98
|
+
assert validate_pr_body(LEGACY_DESCRIPTION_WHY_HOW_BODY) == []
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_validate_passes_sectionless_prose_body() -> None:
|
|
102
|
+
"""Anthropic's trivial-PR shape is one sentence with no headers."""
|
|
103
|
+
body = (
|
|
104
|
+
"Pin third-party GitHub Actions references to immutable commit SHAs "
|
|
105
|
+
"so a tag move cannot redirect CI to attacker-controlled code."
|
|
106
|
+
)
|
|
107
|
+
assert validate_pr_body(body) == []
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_validate_blocks_skeleton_body_with_only_headers_and_bullets() -> None:
|
|
111
|
+
"""Sections + bullets without any prose Why is rejected -- the substantive-prose check catches this."""
|
|
112
|
+
body = "## Summary\n\n## Changes\n\n- `a`\n- `b`\n- `c`\n"
|
|
113
|
+
violations = validate_pr_body(body)
|
|
114
|
+
assert any("substantive prose" in each_violation.lower() for each_violation in violations)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_validate_blocks_blockquoted_headings_with_no_real_prose() -> None:
|
|
118
|
+
"""Regression: blockquote markers must strip BEFORE heading stripping.
|
|
119
|
+
|
|
120
|
+
A line like `> ## Summary` starts with `>`, so `^#+[ \\t].*$` cannot match it
|
|
121
|
+
in heading position. If blockquote markers are stripped after, the bare
|
|
122
|
+
`## Summary` text survives into the prose stream and inflates the count.
|
|
123
|
+
Correct order strips `> ` first, then the line becomes a real heading and
|
|
124
|
+
drops out, leaving an effectively empty body below the 40-character minimum.
|
|
125
|
+
"""
|
|
126
|
+
body = "> ## Summary\n> ## Why\n> ## How"
|
|
127
|
+
violations = validate_pr_body(body)
|
|
128
|
+
assert any("substantive prose" in each_violation.lower() for each_violation in violations)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_validate_passes_prose_after_bare_hashes_with_no_space() -> None:
|
|
132
|
+
"""Bug regression: `##\\n` followed by prose must not have its prose eaten by the heading regex.
|
|
133
|
+
|
|
134
|
+
The previous pattern `^#+\\s.*$` matched `\\s` against the newline, then `.*$` greedily
|
|
135
|
+
consumed the next line. The fix restricts the whitespace class to `[ \\t]` so only true
|
|
136
|
+
headings (`## text`) match, leaving prose-after-bare-hashes intact for substantive-prose counting.
|
|
137
|
+
"""
|
|
138
|
+
body = (
|
|
139
|
+
"##\nThis is real prose that should not be eaten by the heading regex, "
|
|
140
|
+
"it should pass the 40-character minimum."
|
|
141
|
+
)
|
|
142
|
+
assert validate_pr_body(body) == []
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_validate_blocks_vague_language() -> None:
|
|
146
|
+
body = VALID_BODY + "\nFixed bug in the auth module.\n"
|
|
147
|
+
violations = validate_pr_body(body)
|
|
148
|
+
assert any("Vague language" in each_violation for each_violation in violations)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_vague_language_inside_fenced_code_block_is_exempt() -> None:
|
|
152
|
+
body = (
|
|
153
|
+
"The allocator now bounds retries so a runaway request cannot exhaust the "
|
|
154
|
+
"connection pool under sustained load.\n\n"
|
|
155
|
+
'```bash\ngit commit -m "fixed bug in parser"\n```\n'
|
|
156
|
+
)
|
|
157
|
+
assert not _has_vague_language_violation(validate_pr_body(body))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_vague_language_inside_inline_code_span_is_exempt() -> None:
|
|
161
|
+
body = (
|
|
162
|
+
"This change documents the historical commit message `fixed bug` referenced "
|
|
163
|
+
"in the changelog and rewrites the surrounding allocator narrative for clarity.\n"
|
|
164
|
+
)
|
|
165
|
+
assert not _has_vague_language_violation(validate_pr_body(body))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_vague_language_inside_blockquote_line_is_exempt() -> None:
|
|
169
|
+
body = (
|
|
170
|
+
"> The reviewer wrote: minor changes were requested here.\n\n"
|
|
171
|
+
"The allocator rewrite removes the unbounded retry loop and adds a hard ceiling "
|
|
172
|
+
"so a single client cannot starve the pool.\n"
|
|
173
|
+
)
|
|
174
|
+
assert not _has_vague_language_violation(validate_pr_body(body))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_vague_language_inside_markdown_table_is_exempt() -> None:
|
|
178
|
+
body = (
|
|
179
|
+
"The commit-message guide contrasts weak and strong messages so contributors "
|
|
180
|
+
"learn the difference before opening a pull request.\n\n"
|
|
181
|
+
"| Bad message | Good message |\n"
|
|
182
|
+
"| --- | --- |\n"
|
|
183
|
+
"| fixed bug | bound retry loop in allocator |\n"
|
|
184
|
+
"| update code | rename pool field to active_count |\n"
|
|
185
|
+
)
|
|
186
|
+
assert not _has_vague_language_violation(validate_pr_body(body))
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_vague_language_in_bare_prose_still_blocks() -> None:
|
|
190
|
+
body = (
|
|
191
|
+
"The allocator rewrite removes the unbounded retry loop and adds a hard "
|
|
192
|
+
"ceiling so a single client cannot starve the pool. Fixed bug in the parser.\n"
|
|
193
|
+
)
|
|
194
|
+
assert _has_vague_language_violation(validate_pr_body(body))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_vague_language_inside_heading_is_exempt() -> None:
|
|
198
|
+
body = (
|
|
199
|
+
"## Fixed bug in the allocator\n\n"
|
|
200
|
+
"The allocator rewrite removes the unbounded retry loop and adds a hard "
|
|
201
|
+
"ceiling so a single client cannot starve the connection pool.\n"
|
|
202
|
+
)
|
|
203
|
+
assert not _has_vague_language_violation(validate_pr_body(body))
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_vague_language_in_single_pipe_prose_line_still_blocks() -> None:
|
|
207
|
+
body = (
|
|
208
|
+
"The allocator rewrite removes the unbounded retry loop and adds a hard "
|
|
209
|
+
"ceiling so a single client cannot starve the connection pool.\n\n"
|
|
210
|
+
"| fixed bug\n"
|
|
211
|
+
)
|
|
212
|
+
assert _has_vague_language_violation(validate_pr_body(body))
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_validate_blocks_short_body() -> None:
|
|
216
|
+
violations = validate_pr_body("Too short.")
|
|
217
|
+
assert any("substantive prose" in each_violation.lower() for each_violation in violations)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_validate_heavy_body_passes_with_problem_and_test_plan() -> None:
|
|
221
|
+
body = _build_heavy_body("## Problem", "## Test plan")
|
|
222
|
+
assert validate_pr_body(body) == []
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def test_validate_heavy_body_blocks_when_testing_category_missing() -> None:
|
|
226
|
+
"""Heavy body containing two opening-category headers but no testing-category header is blocked."""
|
|
227
|
+
intro_text = (
|
|
228
|
+
"Adds shape-aware validation across the pr-description-enforcer pipeline. "
|
|
229
|
+
"The change unifies the body audit with the Anthropic claude-code style. "
|
|
230
|
+
"The block reason names the missing category for the agent to fix on first try."
|
|
231
|
+
)
|
|
232
|
+
body = (
|
|
233
|
+
f"{intro_text}\n\n"
|
|
234
|
+
"## Summary\n\n"
|
|
235
|
+
"Adds a check that heavy bodies carry both an opening header and a testing header. "
|
|
236
|
+
"The substantive prose lives outside the bullet section so the audit treats the body "
|
|
237
|
+
"as the heavy shape rather than the standard shape under the length threshold.\n\n"
|
|
238
|
+
"## Problem\n\n"
|
|
239
|
+
"The earlier flow rejected too many valid bodies on equivalence checks "
|
|
240
|
+
"across the three shape categories described in the guide. The fix "
|
|
241
|
+
"restructures the path around shape detection and surfaces the missing "
|
|
242
|
+
"category in the block message so the agent can correct it without iterating.\n\n"
|
|
243
|
+
"## Changes\n\n"
|
|
244
|
+
"- `validator.py`: shape detection at the head of the audit pipeline\n"
|
|
245
|
+
"- `enforcer.py`: dispatch the shape-aware checks before the substantive-prose audit\n"
|
|
246
|
+
)
|
|
247
|
+
violations = validate_pr_body(body)
|
|
248
|
+
assert any("testing" in each_violation.lower() for each_violation in violations)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_validate_trivial_body_blocks_summary_header() -> None:
|
|
252
|
+
"""A Trivial-sized body that opens with `## Summary` is blocked as ceremony."""
|
|
253
|
+
body = "## Summary\n\nPin Bun to 1.3.14."
|
|
254
|
+
violations = validate_pr_body(body)
|
|
255
|
+
assert any(
|
|
256
|
+
"ceremony" in each_violation.lower() or "trivial" in each_violation.lower()
|
|
257
|
+
for each_violation in violations
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def test_validate_trivial_body_blocks_test_plan_header() -> None:
|
|
262
|
+
"""A Trivial-sized body that opens with `## Test plan` must trip the
|
|
263
|
+
ceremony-on-Trivial block. The guide says Trivial bodies have zero headers,
|
|
264
|
+
so the enforcer must catch every heading variant — not just the six
|
|
265
|
+
`Summary|Why|Overview|Description|Intro|TL;DR` originally enumerated."""
|
|
266
|
+
body = "## Test plan\n\nPin Bun to 1.3.14."
|
|
267
|
+
violations = validate_pr_body(body)
|
|
268
|
+
assert any(
|
|
269
|
+
"ceremony" in each_violation.lower() or "trivial" in each_violation.lower()
|
|
270
|
+
for each_violation in violations
|
|
271
|
+
), f"Trivial body opening with `## Test plan` must trip ceremony block; got {violations!r}"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_validate_trivial_body_blocks_test_plan_after_prose() -> None:
|
|
275
|
+
"""The doc promises "Zero `##` headers" on Trivial bodies. The earlier check
|
|
276
|
+
only inspected the first non-empty line, so prose followed by `## Test plan`
|
|
277
|
+
slipped through. Tighten the check to reject ANY heading in a Trivial-sized
|
|
278
|
+
body so the guide and the enforcer agree."""
|
|
279
|
+
body = "Pin Bun to 1.3.14.\n\n## Test plan\n\n- bun test\n"
|
|
280
|
+
violations = validate_pr_body(body)
|
|
281
|
+
assert any(
|
|
282
|
+
"ceremony" in each_violation.lower() or "trivial" in each_violation.lower()
|
|
283
|
+
for each_violation in violations
|
|
284
|
+
), f"Trivial body with later `## Test plan` must trip the block; got {violations!r}"
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_validate_trivial_body_blocks_h1_header() -> None:
|
|
288
|
+
"""A Trivial-sized body opening with an `# Overview` h1 must also block, since
|
|
289
|
+
Trivial shape allows zero structural headers of any level."""
|
|
290
|
+
body = "# Overview\n\nPin Bun to 1.3.14."
|
|
291
|
+
violations = validate_pr_body(body)
|
|
292
|
+
assert any(
|
|
293
|
+
"ceremony" in each_violation.lower() or "trivial" in each_violation.lower()
|
|
294
|
+
for each_violation in violations
|
|
295
|
+
), f"Trivial body opening with h1 must trip ceremony block; got {violations!r}"
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_validate_standard_body_allows_summary_header() -> None:
|
|
299
|
+
"""A Standard-sized body that opens with `## Summary` passes the ceremony check."""
|
|
300
|
+
body = (
|
|
301
|
+
"## Summary\n\n"
|
|
302
|
+
"Adds a timestamp check to prevent background data pulls from overwriting "
|
|
303
|
+
"recent local edits. The pull engine compares the last-modified marker "
|
|
304
|
+
"before applying a remote record.\n\n"
|
|
305
|
+
"## Changes\n\n"
|
|
306
|
+
"- `pullEngine.ts`: compare lastModified before overwriting\n"
|
|
307
|
+
"- `pullEngine.test.ts`: 3 new cases\n"
|
|
308
|
+
)
|
|
309
|
+
violations = validate_pr_body(body)
|
|
310
|
+
assert not any(
|
|
311
|
+
"ceremony" in each_violation.lower() or "trivial" in each_violation.lower()
|
|
312
|
+
for each_violation in violations
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def test_validate_blocks_self_closing_fixes_reference() -> None:
|
|
317
|
+
body = (
|
|
318
|
+
"Adds a timestamp check to prevent background data pulls from overwriting "
|
|
319
|
+
"recent local edits.\n\nFixes #467.\n"
|
|
320
|
+
)
|
|
321
|
+
violations = validate_pr_body(body, pr_number=467)
|
|
322
|
+
assert any(
|
|
323
|
+
"self" in each_violation.lower() or "own pr" in each_violation.lower()
|
|
324
|
+
for each_violation in violations
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_validate_blocks_self_closing_resolves_reference() -> None:
|
|
329
|
+
body = (
|
|
330
|
+
"Adds a timestamp check to prevent background data pulls from overwriting "
|
|
331
|
+
"recent local edits.\n\nResolves #467.\n"
|
|
332
|
+
)
|
|
333
|
+
violations = validate_pr_body(body, pr_number=467)
|
|
334
|
+
assert any(
|
|
335
|
+
"self" in each_violation.lower() or "own pr" in each_violation.lower()
|
|
336
|
+
for each_violation in violations
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_validate_blocks_lowercase_self_closing_fixes_reference() -> None:
|
|
341
|
+
"""GitHub treats closing keywords (Fixes/Closes/Resolves) case-insensitively, so
|
|
342
|
+
a body opening with `fixes #<own-PR>` (lowercase) auto-closes the PR on merge
|
|
343
|
+
just like the capitalized form. The enforcer must catch both."""
|
|
344
|
+
body = (
|
|
345
|
+
"Adds a timestamp check to prevent background data pulls from overwriting "
|
|
346
|
+
"recent local edits.\n\nfixes #467.\n"
|
|
347
|
+
)
|
|
348
|
+
violations = validate_pr_body(body, pr_number=467)
|
|
349
|
+
assert any(
|
|
350
|
+
"self" in each_violation.lower() or "own pr" in each_violation.lower()
|
|
351
|
+
for each_violation in violations
|
|
352
|
+
), f"lowercase fixes self-reference must trip the block; got {violations!r}"
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_validate_blocks_self_closing_fix_singular_reference() -> None:
|
|
356
|
+
"""GitHub recognizes nine closing keywords (close/closes/closed,
|
|
357
|
+
fix/fixes/fixed, resolve/resolves/resolved). The bare-stem variants
|
|
358
|
+
`Fix #N`, `Close #N`, `Resolve #N` close the PR on merge just like the
|
|
359
|
+
plural forms, so the enforcer must catch every variant."""
|
|
360
|
+
body = (
|
|
361
|
+
"Adds a timestamp check to prevent background data pulls from overwriting "
|
|
362
|
+
"recent local edits.\n\nFix #467.\n"
|
|
363
|
+
)
|
|
364
|
+
violations = validate_pr_body(body, pr_number=467)
|
|
365
|
+
assert any(
|
|
366
|
+
"self" in each_violation.lower() or "own pr" in each_violation.lower()
|
|
367
|
+
for each_violation in violations
|
|
368
|
+
), f"`Fix #<own-PR>` self-reference must trip the block; got {violations!r}"
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_validate_blocks_self_closing_closed_past_tense_reference() -> None:
|
|
372
|
+
"""`Closed #<own-PR>` (past tense) closes the PR on merge; the enforcer
|
|
373
|
+
must catch every closing-keyword variant including past tense."""
|
|
374
|
+
body = (
|
|
375
|
+
"Adds a timestamp check to prevent background data pulls from overwriting "
|
|
376
|
+
"recent local edits.\n\nClosed #467.\n"
|
|
377
|
+
)
|
|
378
|
+
violations = validate_pr_body(body, pr_number=467)
|
|
379
|
+
assert any(
|
|
380
|
+
"self" in each_violation.lower() or "own pr" in each_violation.lower()
|
|
381
|
+
for each_violation in violations
|
|
382
|
+
), f"`Closed #<own-PR>` self-reference must trip the block; got {violations!r}"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_validate_blocks_self_closing_resolved_past_tense_reference() -> None:
|
|
386
|
+
"""`Resolved #<own-PR>` closes the PR on merge."""
|
|
387
|
+
body = (
|
|
388
|
+
"Adds a timestamp check to prevent background data pulls from overwriting "
|
|
389
|
+
"recent local edits.\n\nResolved #467.\n"
|
|
390
|
+
)
|
|
391
|
+
violations = validate_pr_body(body, pr_number=467)
|
|
392
|
+
assert any(
|
|
393
|
+
"self" in each_violation.lower() or "own pr" in each_violation.lower()
|
|
394
|
+
for each_violation in violations
|
|
395
|
+
), f"`Resolved #<own-PR>` self-reference must trip the block; got {violations!r}"
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_validate_blocks_uppercase_self_closing_closes_reference() -> None:
|
|
399
|
+
"""All-caps `CLOSES #<own-PR>` also auto-closes on GitHub; the enforcer must
|
|
400
|
+
catch every case variant the same way GitHub does."""
|
|
401
|
+
body = (
|
|
402
|
+
"Adds a timestamp check to prevent background data pulls from overwriting "
|
|
403
|
+
"recent local edits.\n\nCLOSES #467.\n"
|
|
404
|
+
)
|
|
405
|
+
violations = validate_pr_body(body, pr_number=467)
|
|
406
|
+
assert any(
|
|
407
|
+
"self" in each_violation.lower() or "own pr" in each_violation.lower()
|
|
408
|
+
for each_violation in violations
|
|
409
|
+
), f"all-caps CLOSES self-reference must trip the block; got {violations!r}"
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_validate_allows_fixes_reference_to_different_pr() -> None:
|
|
413
|
+
body = (
|
|
414
|
+
"Adds a timestamp check to prevent background data pulls from overwriting "
|
|
415
|
+
"recent local edits.\n\nFixes #467.\n"
|
|
416
|
+
)
|
|
417
|
+
violations = validate_pr_body(body, pr_number=999)
|
|
418
|
+
assert not any(
|
|
419
|
+
"self" in each_violation.lower() or "own pr" in each_violation.lower()
|
|
420
|
+
for each_violation in violations
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def test_validate_blocks_this_pr_opening() -> None:
|
|
425
|
+
body = (
|
|
426
|
+
"This PR adds a timestamp check to prevent background data pulls from "
|
|
427
|
+
"overwriting recent local edits. The pull engine compares the "
|
|
428
|
+
"last-modified marker before applying a remote record."
|
|
429
|
+
)
|
|
430
|
+
violations = validate_pr_body(body)
|
|
431
|
+
assert any("this pr" in each_violation.lower() for each_violation in violations)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def test_validate_blocks_this_pr_opening_with_non_allowlisted_verb() -> None:
|
|
435
|
+
"""The guide describes any `This PR ...` opening as a hard block, but
|
|
436
|
+
`THIS_PR_OPENING_PATTERN` previously only matched a short allowlist of
|
|
437
|
+
verbs (adds|fixes|updates|does|is|was|will|removes|tightens|ports|refactors).
|
|
438
|
+
Variants like `This PR introduces`, `This PR improves`, `This PR enables`
|
|
439
|
+
slipped through and broke the documented contract. Catch any
|
|
440
|
+
`This PR` opening regardless of the following verb."""
|
|
441
|
+
body = (
|
|
442
|
+
"This PR introduces a multi-tier caching layer that wraps the existing "
|
|
443
|
+
"request pipeline and improves median latency on the hot path."
|
|
444
|
+
)
|
|
445
|
+
violations = validate_pr_body(body)
|
|
446
|
+
assert any("this pr" in each_violation.lower() for each_violation in violations), (
|
|
447
|
+
f"`This PR introduces` opening must trip the block regardless of verb; got {violations!r}"
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def test_validate_blocks_this_pr_opening_with_improves() -> None:
|
|
452
|
+
body = (
|
|
453
|
+
"This PR improves the request batching algorithm so the dispatcher "
|
|
454
|
+
"coalesces idempotent calls before the network round-trip."
|
|
455
|
+
)
|
|
456
|
+
violations = validate_pr_body(body)
|
|
457
|
+
assert any("this pr" in each_violation.lower() for each_violation in violations), (
|
|
458
|
+
f"`This PR improves` opening must trip the block; got {violations!r}"
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def test_validate_allows_imperative_opening() -> None:
|
|
463
|
+
body = (
|
|
464
|
+
"Adds a timestamp check to prevent background data pulls from "
|
|
465
|
+
"overwriting recent local edits. The pull engine compares the "
|
|
466
|
+
"last-modified marker before applying a remote record."
|
|
467
|
+
)
|
|
468
|
+
violations = validate_pr_body(body)
|
|
469
|
+
assert not any("this pr" in each_violation.lower() for each_violation in violations)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def test_validate_heavy_body_without_required_headers_blocks() -> None:
|
|
473
|
+
"""End-to-end: a long body without `## Problem|Summary` or `## Test plan|...`
|
|
474
|
+
must trip the Heavy missing-header violation. Previously the classifier
|
|
475
|
+
bypassed Heavy classification because the body lacked the headers we were
|
|
476
|
+
trying to require — a circular self-bypass."""
|
|
477
|
+
long_body_missing_heavy_headers = (
|
|
478
|
+
"Refactors the request-pipeline batcher to coalesce idempotent calls "
|
|
479
|
+
"before the network round-trip. The change touches the dispatcher, the "
|
|
480
|
+
"retry loop, the error normalizer, and three downstream consumers. "
|
|
481
|
+
"Every test in the integration suite continues to pass without "
|
|
482
|
+
"modification because the public contract is unchanged.\n\n"
|
|
483
|
+
"The new coalescer reads a per-call digest, looks up an in-flight slot "
|
|
484
|
+
"indexed by that digest, and appends the caller's promise to the slot "
|
|
485
|
+
"instead of dispatching a duplicate request. Once the network response "
|
|
486
|
+
"arrives, every queued promise resolves with the same value. Error "
|
|
487
|
+
"responses propagate to every queued promise so retry logic stays "
|
|
488
|
+
"consistent with the prior contract.\n"
|
|
489
|
+
)
|
|
490
|
+
violations = validate_pr_body(long_body_missing_heavy_headers)
|
|
491
|
+
assert any("heavy" in each_violation.lower() for each_violation in violations), (
|
|
492
|
+
f"Long body missing Heavy headers must trip the required-header check; got {violations!r}"
|
|
493
|
+
)
|