claude-dev-env 1.59.0 → 1.60.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.
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +1 -1
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
- package/hooks/blocking/code_rules_dead_module_constant.py +321 -0
- package/hooks/blocking/code_rules_duplicate_body.py +152 -0
- package/hooks/blocking/code_rules_enforcer.py +30 -15
- package/hooks/blocking/code_rules_typeddict_stub.py +172 -0
- package/hooks/blocking/config/__init__.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +146 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +188 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +415 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +156 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +720 -0
- package/hooks/blocking/test_verification_verdict_store.py +278 -0
- package/hooks/blocking/test_verified_commit_gate.py +368 -0
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +214 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +667 -0
- package/hooks/blocking/verification_verdict_store.py +446 -0
- package/hooks/blocking/verified_commit_gate.py +523 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +152 -0
- package/hooks/blocking/verifier_verdict_minter.py +299 -0
- package/hooks/diagnostic/test_hook_log_extractor.py +3 -3
- package/hooks/hooks.json +43 -1
- package/hooks/hooks_constants/blocking_check_limits.py +1 -0
- package/hooks/hooks_constants/dead_module_constant_constants.py +20 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +22 -5
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +1 -1
- package/package.json +1 -1
- package/rules/file-global-constants.md +7 -1
- package/rules/no-cross-skill-duplicate-helpers.md +29 -0
- package/skills/_shared/pr-loop/scripts/preflight_worktree.py +392 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +70 -0
- package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +263 -0
- package/skills/autoconverge/SKILL.md +54 -17
- package/skills/autoconverge/reference/closing-report.md +59 -17
- package/skills/autoconverge/workflow/aggregate_runs.py +371 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +193 -76
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +76 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +206 -206
- package/skills/autoconverge/workflow/converge.mjs +128 -6
- package/skills/autoconverge/workflow/convergence_summary.py +110 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +7 -0
- package/skills/autoconverge/workflow/render_report.py +488 -397
- package/skills/autoconverge/workflow/test_aggregate_runs.py +134 -0
- package/skills/autoconverge/workflow/test_convergence_summary.py +132 -0
- package/skills/autoconverge/workflow/test_render_report.py +488 -259
- package/skills/pr-converge/reference/per-tick.md +28 -8
- package/skills/rebase/SKILL.md +2 -4
- package/system-prompts/software-engineer.xml +2 -6
- package/hooks/blocking/content_search_to_zoekt_redirector.py +0 -59
- package/hooks/blocking/content_search_zoekt_bash_block_reason.py +0 -25
- package/hooks/blocking/content_search_zoekt_block_payload.py +0 -21
- package/hooks/blocking/content_search_zoekt_indexed_paths.py +0 -24
- package/hooks/blocking/content_search_zoekt_indexed_roots_config.py +0 -131
- package/hooks/blocking/content_search_zoekt_redirect_guidance.py +0 -52
- package/hooks/blocking/test_content_search_to_zoekt_redirector_integration.py +0 -61
- package/hooks/blocking/test_content_search_to_zoekt_redirector_unit.py +0 -92
- package/hooks/blocking/test_content_search_zoekt_indexed_roots_config.py +0 -102
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""Tests for the mechanical commit-gate exemption in verification_verdict_store.
|
|
2
|
+
|
|
3
|
+
Each test builds a real git repository with a real origin remote and asserts
|
|
4
|
+
the exemption decision against the live work tree, exercising the same code
|
|
5
|
+
path the verified_commit_gate hook runs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
import pathlib
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
14
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
15
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
16
|
+
|
|
17
|
+
store_spec = importlib.util.spec_from_file_location(
|
|
18
|
+
"verification_verdict_store",
|
|
19
|
+
_HOOK_DIR / "verification_verdict_store.py",
|
|
20
|
+
)
|
|
21
|
+
assert store_spec is not None
|
|
22
|
+
assert store_spec.loader is not None
|
|
23
|
+
store_module = importlib.util.module_from_spec(store_spec)
|
|
24
|
+
store_spec.loader.exec_module(store_module)
|
|
25
|
+
is_verification_exempt_diff = store_module.is_verification_exempt_diff
|
|
26
|
+
resolve_merge_base = store_module.resolve_merge_base
|
|
27
|
+
branch_surface_manifest = store_module.branch_surface_manifest
|
|
28
|
+
|
|
29
|
+
constants_spec = importlib.util.spec_from_file_location(
|
|
30
|
+
"verified_commit_constants",
|
|
31
|
+
_HOOK_DIR / "config" / "verified_commit_constants.py",
|
|
32
|
+
)
|
|
33
|
+
assert constants_spec is not None
|
|
34
|
+
assert constants_spec.loader is not None
|
|
35
|
+
constants_module = importlib.util.module_from_spec(constants_spec)
|
|
36
|
+
constants_spec.loader.exec_module(constants_module)
|
|
37
|
+
CORRECTIVE_MESSAGE = constants_module.CORRECTIVE_MESSAGE
|
|
38
|
+
|
|
39
|
+
PRODUCTION_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
|
|
40
|
+
TEST_SOURCE = "def test_add() -> None:\n assert 1 + 1 == 2\n"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _run_git(repo_dir: pathlib.Path, *git_arguments: str) -> None:
|
|
44
|
+
subprocess.run(
|
|
45
|
+
["git", "-C", str(repo_dir), *git_arguments],
|
|
46
|
+
check=True,
|
|
47
|
+
capture_output=True,
|
|
48
|
+
text=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _make_repo_on_branch(
|
|
53
|
+
tmp_path: pathlib.Path, branch_name: str
|
|
54
|
+
) -> pathlib.Path:
|
|
55
|
+
origin_dir = tmp_path / "origin.git"
|
|
56
|
+
work_dir = tmp_path / "work"
|
|
57
|
+
work_dir.mkdir()
|
|
58
|
+
subprocess.run(
|
|
59
|
+
["git", "init", "--bare", f"--initial-branch={branch_name}", str(origin_dir)],
|
|
60
|
+
check=True,
|
|
61
|
+
capture_output=True,
|
|
62
|
+
text=True,
|
|
63
|
+
)
|
|
64
|
+
empty_hooks_dir = tmp_path / "nohooks"
|
|
65
|
+
empty_hooks_dir.mkdir()
|
|
66
|
+
_run_git(work_dir, "init", f"--initial-branch={branch_name}")
|
|
67
|
+
_run_git(work_dir, "config", "user.email", "tests@example.com")
|
|
68
|
+
_run_git(work_dir, "config", "user.name", "Verdict Store Tests")
|
|
69
|
+
_run_git(work_dir, "config", "core.hooksPath", str(empty_hooks_dir))
|
|
70
|
+
(work_dir / "src").mkdir()
|
|
71
|
+
(work_dir / "tests").mkdir()
|
|
72
|
+
(work_dir / "src" / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
|
|
73
|
+
(work_dir / "tests" / "test_app.py").write_text(TEST_SOURCE, encoding="utf-8")
|
|
74
|
+
(work_dir / "README.md").write_text("# Fixture repo\n", encoding="utf-8")
|
|
75
|
+
_run_git(work_dir, "add", "-A")
|
|
76
|
+
_run_git(work_dir, "commit", "-m", "base")
|
|
77
|
+
_run_git(work_dir, "remote", "add", "origin", str(origin_dir))
|
|
78
|
+
_run_git(work_dir, "push", "-u", "origin", branch_name)
|
|
79
|
+
return work_dir
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _make_repo_with_origin(tmp_path: pathlib.Path) -> pathlib.Path:
|
|
83
|
+
return _make_repo_on_branch(tmp_path, "main")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _exemption_for(work_dir: pathlib.Path) -> bool:
|
|
87
|
+
merge_base_sha = resolve_merge_base(str(work_dir))
|
|
88
|
+
assert merge_base_sha is not None
|
|
89
|
+
return is_verification_exempt_diff(str(work_dir), merge_base_sha)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_production_change_is_gated(tmp_path: pathlib.Path) -> None:
|
|
93
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
94
|
+
(work_dir / "src" / "app.py").write_text(
|
|
95
|
+
"def add(left: int, right: int) -> int:\n return left - right\n",
|
|
96
|
+
encoding="utf-8",
|
|
97
|
+
)
|
|
98
|
+
assert _exemption_for(work_dir) is False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_docs_only_change_is_exempt(tmp_path: pathlib.Path) -> None:
|
|
102
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
103
|
+
(work_dir / "README.md").write_text(
|
|
104
|
+
"# Fixture repo\n\nUpdated.\n", encoding="utf-8"
|
|
105
|
+
)
|
|
106
|
+
assert _exemption_for(work_dir) is True
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_docstring_only_python_change_is_exempt(tmp_path: pathlib.Path) -> None:
|
|
110
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
111
|
+
(work_dir / "src" / "app.py").write_text(
|
|
112
|
+
'def add(left: int, right: int) -> int:\n """Add two integers."""\n'
|
|
113
|
+
" return left + right\n",
|
|
114
|
+
encoding="utf-8",
|
|
115
|
+
)
|
|
116
|
+
assert _exemption_for(work_dir) is True
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_modified_test_file_is_exempt(tmp_path: pathlib.Path) -> None:
|
|
120
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
121
|
+
(work_dir / "tests" / "test_app.py").write_text(
|
|
122
|
+
TEST_SOURCE + "\n\ndef test_add_zero() -> None:\n assert 0 + 0 == 0\n",
|
|
123
|
+
encoding="utf-8",
|
|
124
|
+
)
|
|
125
|
+
assert _exemption_for(work_dir) is True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_untracked_test_prefix_file_is_exempt(tmp_path: pathlib.Path) -> None:
|
|
129
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
130
|
+
(work_dir / "tests" / "test_extra.py").write_text(TEST_SOURCE, encoding="utf-8")
|
|
131
|
+
assert _exemption_for(work_dir) is True
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_untracked_test_suffix_file_is_exempt(tmp_path: pathlib.Path) -> None:
|
|
135
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
136
|
+
(work_dir / "tests" / "app_test.py").write_text(TEST_SOURCE, encoding="utf-8")
|
|
137
|
+
assert _exemption_for(work_dir) is True
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_modified_conftest_is_exempt(tmp_path: pathlib.Path) -> None:
|
|
141
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
142
|
+
(work_dir / "tests" / "conftest.py").write_text(
|
|
143
|
+
"import pytest\n\n\n@pytest.fixture\ndef sample() -> int:\n return 3\n",
|
|
144
|
+
encoding="utf-8",
|
|
145
|
+
)
|
|
146
|
+
assert _exemption_for(work_dir) is True
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_deleted_test_file_is_exempt(tmp_path: pathlib.Path) -> None:
|
|
150
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
151
|
+
(work_dir / "tests" / "test_app.py").unlink()
|
|
152
|
+
assert _exemption_for(work_dir) is True
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_mixed_test_and_production_change_is_gated(tmp_path: pathlib.Path) -> None:
|
|
156
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
157
|
+
(work_dir / "tests" / "test_app.py").write_text(
|
|
158
|
+
TEST_SOURCE + "\n", encoding="utf-8"
|
|
159
|
+
)
|
|
160
|
+
(work_dir / "src" / "app.py").write_text(
|
|
161
|
+
"def add(left: int, right: int) -> int:\n return left * right\n",
|
|
162
|
+
encoding="utf-8",
|
|
163
|
+
)
|
|
164
|
+
assert _exemption_for(work_dir) is False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_untracked_production_file_is_gated(tmp_path: pathlib.Path) -> None:
|
|
168
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
169
|
+
(work_dir / "src" / "extra.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
|
|
170
|
+
assert _exemption_for(work_dir) is False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_production_file_named_like_test_outside_python_is_gated(
|
|
174
|
+
tmp_path: pathlib.Path,
|
|
175
|
+
) -> None:
|
|
176
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
177
|
+
(work_dir / "src" / "test_data.json").write_text("{}", encoding="utf-8")
|
|
178
|
+
assert _exemption_for(work_dir) is False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_comment_only_change_in_non_python_file_is_gated(
|
|
182
|
+
tmp_path: pathlib.Path,
|
|
183
|
+
) -> None:
|
|
184
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
185
|
+
shell_script_path = work_dir / "src" / "deploy.sh"
|
|
186
|
+
shell_script_path.write_text("# build the project\nmake build\n", encoding="utf-8")
|
|
187
|
+
_run_git(work_dir, "add", "-A")
|
|
188
|
+
_run_git(work_dir, "commit", "-m", "add deploy script")
|
|
189
|
+
shell_script_path.write_text(
|
|
190
|
+
"# build the release artifact\nmake build\n", encoding="utf-8"
|
|
191
|
+
)
|
|
192
|
+
assert _exemption_for(work_dir) is False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_corrective_message_scopes_comment_exemption_to_python() -> None:
|
|
196
|
+
lowered_message = CORRECTIVE_MESSAGE.lower()
|
|
197
|
+
assert "comment" in lowered_message
|
|
198
|
+
assert "python" in lowered_message
|
|
199
|
+
assert "comment-, and test-only surfaces are exempt" not in lowered_message
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_untracked_claude_production_hook_is_gated(tmp_path: pathlib.Path) -> None:
|
|
203
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
204
|
+
new_hook_dir = work_dir / ".claude" / "hooks" / "blocking"
|
|
205
|
+
new_hook_dir.mkdir(parents=True)
|
|
206
|
+
(new_hook_dir / "evil_new_hook.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
|
|
207
|
+
assert _exemption_for(work_dir) is False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_untracked_claude_production_hook_is_in_surface_manifest(
|
|
211
|
+
tmp_path: pathlib.Path,
|
|
212
|
+
) -> None:
|
|
213
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
214
|
+
new_hook_dir = work_dir / ".claude" / "hooks" / "blocking"
|
|
215
|
+
new_hook_dir.mkdir(parents=True)
|
|
216
|
+
(new_hook_dir / "evil_new_hook.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
|
|
217
|
+
merge_base_sha = resolve_merge_base(str(work_dir))
|
|
218
|
+
assert merge_base_sha is not None
|
|
219
|
+
surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
|
|
220
|
+
assert surface_manifest_text is not None
|
|
221
|
+
assert ".claude/hooks/blocking/evil_new_hook.py" in surface_manifest_text
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_untracked_claude_worktree_scratch_copy_stays_filtered(
|
|
225
|
+
tmp_path: pathlib.Path,
|
|
226
|
+
) -> None:
|
|
227
|
+
work_dir = _make_repo_with_origin(tmp_path)
|
|
228
|
+
scratch_dir = work_dir / ".claude" / "worktrees" / "feature" / "src"
|
|
229
|
+
scratch_dir.mkdir(parents=True)
|
|
230
|
+
(scratch_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
|
|
231
|
+
assert _exemption_for(work_dir) is True
|
|
232
|
+
merge_base_sha = resolve_merge_base(str(work_dir))
|
|
233
|
+
assert merge_base_sha is not None
|
|
234
|
+
surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
|
|
235
|
+
assert surface_manifest_text == ""
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _git_output(work_dir: pathlib.Path, *git_arguments: str) -> str:
|
|
239
|
+
completed_process = subprocess.run(
|
|
240
|
+
["git", "-C", str(work_dir), *git_arguments],
|
|
241
|
+
check=True,
|
|
242
|
+
capture_output=True,
|
|
243
|
+
text=True,
|
|
244
|
+
)
|
|
245
|
+
return completed_process.stdout.strip()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_resolve_merge_base_finds_nonstandard_default_branch(
|
|
249
|
+
tmp_path: pathlib.Path,
|
|
250
|
+
) -> None:
|
|
251
|
+
work_dir = _make_repo_on_branch(tmp_path, "develop")
|
|
252
|
+
subprocess.run(
|
|
253
|
+
["git", "-C", str(work_dir), "remote", "set-head", "origin", "--delete"],
|
|
254
|
+
check=True,
|
|
255
|
+
capture_output=True,
|
|
256
|
+
text=True,
|
|
257
|
+
)
|
|
258
|
+
expected_merge_base = _git_output(
|
|
259
|
+
work_dir, "merge-base", "HEAD", "origin/develop"
|
|
260
|
+
)
|
|
261
|
+
assert resolve_merge_base(str(work_dir)) == expected_merge_base
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_production_change_is_gated_on_nonstandard_default_branch(
|
|
265
|
+
tmp_path: pathlib.Path,
|
|
266
|
+
) -> None:
|
|
267
|
+
work_dir = _make_repo_on_branch(tmp_path, "develop")
|
|
268
|
+
subprocess.run(
|
|
269
|
+
["git", "-C", str(work_dir), "remote", "set-head", "origin", "--delete"],
|
|
270
|
+
check=True,
|
|
271
|
+
capture_output=True,
|
|
272
|
+
text=True,
|
|
273
|
+
)
|
|
274
|
+
(work_dir / "src" / "app.py").write_text(
|
|
275
|
+
"def add(left: int, right: int) -> int:\n return left - right\n",
|
|
276
|
+
encoding="utf-8",
|
|
277
|
+
)
|
|
278
|
+
assert _exemption_for(work_dir) is False
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Tests for the gated-invocation parser in verified_commit_gate.
|
|
2
|
+
|
|
3
|
+
Each test asserts which directories a command string's git commit/push verbs
|
|
4
|
+
target, exercising the same token-walk the verified_commit_gate hook runs to
|
|
5
|
+
decide what to gate.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
import os
|
|
10
|
+
import pathlib
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
16
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
18
|
+
|
|
19
|
+
gate_spec = importlib.util.spec_from_file_location(
|
|
20
|
+
"verified_commit_gate",
|
|
21
|
+
_HOOK_DIR / "verified_commit_gate.py",
|
|
22
|
+
)
|
|
23
|
+
assert gate_spec is not None
|
|
24
|
+
assert gate_spec.loader is not None
|
|
25
|
+
gate_module = importlib.util.module_from_spec(gate_spec)
|
|
26
|
+
gate_spec.loader.exec_module(gate_module)
|
|
27
|
+
gated_repo_directories = gate_module.gated_repo_directories
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_plain_git_commit_is_gated() -> None:
|
|
31
|
+
assert gated_repo_directories("git commit -m x", "FALLBACK") == ["FALLBACK"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_git_exe_commit_is_gated() -> None:
|
|
35
|
+
assert gated_repo_directories("git.exe commit -m x", "FALLBACK") == ["FALLBACK"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_git_exe_push_is_gated() -> None:
|
|
39
|
+
assert gated_repo_directories("git.exe push origin main", "FALLBACK") == [
|
|
40
|
+
"FALLBACK"
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_git_exe_commit_records_repo_directory_option() -> None:
|
|
45
|
+
assert gated_repo_directories('git.exe -C "/repo" commit -m x', "FALLBACK") == [
|
|
46
|
+
"/repo"
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_git_log_grep_commit_is_not_gated() -> None:
|
|
51
|
+
assert gated_repo_directories("git log --grep commit", "FALLBACK") == []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_git_stash_push_is_not_gated() -> None:
|
|
55
|
+
assert gated_repo_directories("git stash push", "FALLBACK") == []
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_git_dir_option_value_yields_single_entry() -> None:
|
|
59
|
+
assert gated_repo_directories(
|
|
60
|
+
"git --git-dir=/x/.git commit", "FALLBACK"
|
|
61
|
+
) == ["FALLBACK"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_substring_git_in_branch_name_is_not_a_git_word() -> None:
|
|
65
|
+
assert gated_repo_directories("git push origin legit-branch", "FALLBACK") == [
|
|
66
|
+
"FALLBACK"
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_two_real_git_commits_yield_two_entries() -> None:
|
|
71
|
+
assert gated_repo_directories(
|
|
72
|
+
"git commit -m x && git push origin main", "FALLBACK"
|
|
73
|
+
) == ["FALLBACK", "FALLBACK"]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_unix_path_prefixed_git_commit_is_gated() -> None:
|
|
77
|
+
assert gated_repo_directories("/usr/bin/git commit -m x", "FALLBACK") == [
|
|
78
|
+
"FALLBACK"
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_windows_path_prefixed_git_exe_commit_is_gated() -> None:
|
|
83
|
+
assert gated_repo_directories(
|
|
84
|
+
"C:/Program Files/Git/bin/git.exe commit -m x", "FALLBACK"
|
|
85
|
+
) == ["FALLBACK"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_backslash_path_prefixed_git_exe_commit_is_gated() -> None:
|
|
89
|
+
assert gated_repo_directories(
|
|
90
|
+
"C:\\Program Files\\Git\\cmd\\git.exe commit", "FALLBACK"
|
|
91
|
+
) == ["FALLBACK"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_quoted_git_word_commit_is_gated() -> None:
|
|
95
|
+
assert gated_repo_directories('"git" commit -m x', "FALLBACK") == ["FALLBACK"]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_call_operator_path_git_exe_commit_is_gated() -> None:
|
|
99
|
+
assert gated_repo_directories("& 'C:/x/git.exe' commit", "FALLBACK") == [
|
|
100
|
+
"FALLBACK"
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_call_operator_program_files_git_exe_commit_is_gated() -> None:
|
|
105
|
+
assert gated_repo_directories(
|
|
106
|
+
'& "C:\\Program Files\\Git\\cmd\\git.exe" commit -m x', "C:/repo"
|
|
107
|
+
) == ["C:/repo"]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_quoted_program_files_git_exe_commit_is_gated() -> None:
|
|
111
|
+
assert gated_repo_directories(
|
|
112
|
+
'"C:/Program Files/Git/cmd/git.exe" commit', "C:/repo"
|
|
113
|
+
) == ["C:/repo"]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_quoted_program_files_path_without_git_segment_is_not_gated() -> None:
|
|
117
|
+
assert gated_repo_directories(
|
|
118
|
+
'"C:/Program Files/Other/tool.exe" commit', "C:/repo"
|
|
119
|
+
) == []
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_path_prefixed_git_log_grep_commit_is_not_gated() -> None:
|
|
123
|
+
assert gated_repo_directories("/usr/bin/git log --grep commit", "FALLBACK") == []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_mygit_commit_is_not_a_git_word() -> None:
|
|
127
|
+
assert gated_repo_directories("mygit commit", "FALLBACK") == []
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_legit_commit_is_not_a_git_word() -> None:
|
|
131
|
+
assert gated_repo_directories("legit commit", "FALLBACK") == []
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_cd_then_git_commit_gates_the_cd_directory() -> None:
|
|
135
|
+
assert gated_repo_directories(
|
|
136
|
+
"cd /other/repo && git commit -m x", "FALLBACK"
|
|
137
|
+
) == ["/other/repo"]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_pushd_then_git_commit_gates_the_pushd_directory() -> None:
|
|
141
|
+
assert gated_repo_directories(
|
|
142
|
+
"pushd /other/repo; git commit -m x", "FALLBACK"
|
|
143
|
+
) == ["/other/repo"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_cd_with_quoted_directory_gates_the_quoted_directory() -> None:
|
|
147
|
+
assert gated_repo_directories(
|
|
148
|
+
'cd "/path with spaces" && git commit -m x', "FALLBACK"
|
|
149
|
+
) == ["/path with spaces"]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_explicit_repo_option_overrides_cd_directory() -> None:
|
|
153
|
+
assert gated_repo_directories(
|
|
154
|
+
'cd /other/repo && git -C "/repo" commit -m x', "FALLBACK"
|
|
155
|
+
) == ["/repo"]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_later_cd_applies_to_later_git_commit() -> None:
|
|
159
|
+
assert gated_repo_directories(
|
|
160
|
+
"git commit -m a && cd /second && git commit -m b", "FALLBACK"
|
|
161
|
+
) == ["FALLBACK", "/second"]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_cd_without_argument_keeps_the_fallback_directory() -> None:
|
|
165
|
+
assert gated_repo_directories("cd && git commit -m x", "FALLBACK") == [
|
|
166
|
+
"FALLBACK"
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_set_location_then_git_commit_gates_the_set_location_directory() -> None:
|
|
171
|
+
assert gated_repo_directories(
|
|
172
|
+
"Set-Location /other/repo; git commit -m x", "FALLBACK"
|
|
173
|
+
) == ["/other/repo"]
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_sl_alias_then_git_commit_gates_the_sl_directory() -> None:
|
|
177
|
+
assert gated_repo_directories(
|
|
178
|
+
"sl /other/repo; git commit -m x", "FALLBACK"
|
|
179
|
+
) == ["/other/repo"]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_set_location_is_matched_case_insensitively() -> None:
|
|
183
|
+
assert gated_repo_directories(
|
|
184
|
+
"set-location /other/repo; git commit -m x", "FALLBACK"
|
|
185
|
+
) == ["/other/repo"]
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_relative_cd_target_resolves_against_the_fallback_directory() -> None:
|
|
189
|
+
expected_directory = os.path.join("/session/dir", "subdir")
|
|
190
|
+
assert gated_repo_directories(
|
|
191
|
+
"cd subdir && git commit -m x", "/session/dir"
|
|
192
|
+
) == [expected_directory]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_relative_set_location_target_resolves_against_the_fallback_directory() -> None:
|
|
196
|
+
expected_directory = os.path.join("/session/dir", "subdir")
|
|
197
|
+
assert gated_repo_directories(
|
|
198
|
+
"Set-Location subdir; git commit -m x", "/session/dir"
|
|
199
|
+
) == [expected_directory]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_absolute_cd_target_is_not_joined_to_the_fallback_directory() -> None:
|
|
203
|
+
assert gated_repo_directories(
|
|
204
|
+
"cd /other/repo && git commit -m x", "/session/dir"
|
|
205
|
+
) == ["/other/repo"]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_work_tree_option_gates_the_work_tree_directory() -> None:
|
|
209
|
+
assert gated_repo_directories(
|
|
210
|
+
"git --git-dir=/other/.git --work-tree=/other commit", "/session"
|
|
211
|
+
) == ["/other"]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_work_tree_space_separated_option_gates_the_work_tree_directory() -> None:
|
|
215
|
+
assert gated_repo_directories(
|
|
216
|
+
"git --work-tree /other commit -m x", "/session"
|
|
217
|
+
) == ["/other"]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_repo_option_overrides_work_tree_option() -> None:
|
|
221
|
+
assert gated_repo_directories(
|
|
222
|
+
'git -C "/repo" --work-tree=/other commit', "/session"
|
|
223
|
+
) == ["/repo"]
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def test_relative_repo_option_resolves_against_the_active_directory() -> None:
|
|
227
|
+
expected_directory = os.path.join("/repo", "subdir")
|
|
228
|
+
assert gated_repo_directories(
|
|
229
|
+
"cd /repo && git -C subdir commit -m x", "FALLBACK"
|
|
230
|
+
) == [expected_directory]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_relative_work_tree_option_resolves_against_the_active_directory() -> None:
|
|
234
|
+
expected_directory = os.path.join("/repo", "subtree")
|
|
235
|
+
assert gated_repo_directories(
|
|
236
|
+
"cd /repo && git --work-tree subtree commit -m x", "FALLBACK"
|
|
237
|
+
) == [expected_directory]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_relative_repo_option_resolves_against_the_fallback_directory() -> None:
|
|
241
|
+
expected_directory = os.path.join("/session/dir", "subdir")
|
|
242
|
+
assert gated_repo_directories(
|
|
243
|
+
"git -C subdir commit -m x", "/session/dir"
|
|
244
|
+
) == [expected_directory]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _set_fake_home(monkeypatch: pytest.MonkeyPatch, fake_home: pathlib.Path) -> str:
|
|
248
|
+
home_text = str(fake_home)
|
|
249
|
+
monkeypatch.setenv("HOME", home_text)
|
|
250
|
+
monkeypatch.setenv("USERPROFILE", home_text)
|
|
251
|
+
monkeypatch.delenv("HOMEDRIVE", raising=False)
|
|
252
|
+
monkeypatch.delenv("HOMEPATH", raising=False)
|
|
253
|
+
return home_text
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_cd_into_tilde_repo_gates_the_expanded_home_path(
|
|
257
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
258
|
+
) -> None:
|
|
259
|
+
home_text = _set_fake_home(monkeypatch, tmp_path)
|
|
260
|
+
expected_directory = os.path.expanduser("~/myrepo")
|
|
261
|
+
assert home_text in expected_directory
|
|
262
|
+
assert gated_repo_directories(
|
|
263
|
+
"cd ~/myrepo && git commit -m x", "FALLBACK"
|
|
264
|
+
) == [expected_directory]
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_git_dash_c_tilde_repo_gates_the_expanded_home_path(
|
|
268
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
269
|
+
) -> None:
|
|
270
|
+
home_text = _set_fake_home(monkeypatch, tmp_path)
|
|
271
|
+
expected_directory = os.path.expanduser("~/myrepo")
|
|
272
|
+
assert home_text in expected_directory
|
|
273
|
+
assert gated_repo_directories(
|
|
274
|
+
"git -C ~/myrepo commit -m x", "FALLBACK"
|
|
275
|
+
) == [expected_directory]
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_git_work_tree_tilde_repo_gates_the_expanded_home_path(
|
|
279
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
280
|
+
) -> None:
|
|
281
|
+
home_text = _set_fake_home(monkeypatch, tmp_path)
|
|
282
|
+
expected_directory = os.path.expanduser("~/myrepo")
|
|
283
|
+
assert home_text in expected_directory
|
|
284
|
+
assert gated_repo_directories(
|
|
285
|
+
"git --work-tree ~/myrepo commit -m x", "FALLBACK"
|
|
286
|
+
) == [expected_directory]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_set_location_path_option_gates_the_path_value() -> None:
|
|
290
|
+
assert gated_repo_directories(
|
|
291
|
+
"Set-Location -Path /repo; git commit -m x", "FALLBACK"
|
|
292
|
+
) == ["/repo"]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def test_pushd_literal_path_option_gates_the_path_value() -> None:
|
|
296
|
+
assert gated_repo_directories(
|
|
297
|
+
"pushd -LiteralPath /repo; git commit", "FALLBACK"
|
|
298
|
+
) == ["/repo"]
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_cd_double_dash_terminator_gates_the_path_value() -> None:
|
|
302
|
+
assert gated_repo_directories(
|
|
303
|
+
"cd -- /repo && git commit", "FALLBACK"
|
|
304
|
+
) == ["/repo"]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_cd_inside_double_quoted_commit_message_does_not_divert_later_push() -> None:
|
|
308
|
+
assert gated_repo_directories(
|
|
309
|
+
'git commit -m "context: cd /var/empty/nonrepo first" && git push',
|
|
310
|
+
"/real/repo/worktree",
|
|
311
|
+
) == ["/real/repo/worktree", "/real/repo/worktree"]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def test_cd_inside_single_quoted_commit_message_does_not_divert_later_push() -> None:
|
|
315
|
+
assert gated_repo_directories(
|
|
316
|
+
"git commit -m 'context: cd /var/empty/nonrepo first' && git push",
|
|
317
|
+
"/real/repo/worktree",
|
|
318
|
+
) == ["/real/repo/worktree", "/real/repo/worktree"]
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def test_pushd_inside_quoted_commit_message_does_not_divert_later_push() -> None:
|
|
322
|
+
assert gated_repo_directories(
|
|
323
|
+
'git commit -m "context: pushd /var/empty/nonrepo first" && git push',
|
|
324
|
+
"/real/repo/worktree",
|
|
325
|
+
) == ["/real/repo/worktree", "/real/repo/worktree"]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_real_cd_outside_quotes_still_diverts_after_a_quoted_message() -> None:
|
|
329
|
+
assert gated_repo_directories(
|
|
330
|
+
'git commit -m "msg" && cd /other/repo && git push',
|
|
331
|
+
"/real/repo/worktree",
|
|
332
|
+
) == ["/real/repo/worktree", "/other/repo"]
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def test_git_word_inside_quoted_commit_message_yields_one_entry() -> None:
|
|
336
|
+
assert gated_repo_directories(
|
|
337
|
+
'git commit -m "remember to git commit later"', "FB"
|
|
338
|
+
) == ["FB"]
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_git_word_inside_single_quoted_message_value_yields_one_entry() -> None:
|
|
342
|
+
assert gated_repo_directories(
|
|
343
|
+
"git commit -m 'remember to git commit later'", "FB"
|
|
344
|
+
) == ["FB"]
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_backslash_newline_after_git_word_is_gated() -> None:
|
|
348
|
+
assert gated_repo_directories("git \\\n commit -m x", "FB") == ["FB"]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_backslash_newline_abutting_subcommand_is_gated() -> None:
|
|
352
|
+
assert gated_repo_directories("git commit\\\n -m x", "FB") == ["FB"]
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def test_backslash_newline_splitting_git_word_is_gated() -> None:
|
|
356
|
+
assert gated_repo_directories("g\\\nit commit -m x", "FB") == ["FB"]
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def test_git_verb_inside_echo_prose_is_not_gated() -> None:
|
|
360
|
+
assert gated_repo_directories(
|
|
361
|
+
'echo "Next: git commit and git push"', "/d"
|
|
362
|
+
) == []
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def test_git_verb_inside_gh_comment_body_is_not_gated() -> None:
|
|
366
|
+
assert gated_repo_directories(
|
|
367
|
+
'gh pr comment -b "please git commit your work"', "/d"
|
|
368
|
+
) == []
|